From a364f50bdd0c789d41f3b2f7d5c73b8945bab1a3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 23:45:55 +0000 Subject: [PATCH 01/71] Update dependency setuptools to v80 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ddf1ba51a..a4ee2fb8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools~=79.0"] +requires = ["setuptools~=80.0"] build-backend = "setuptools.build_meta" [project] From 71637e852bbf21f7bf7020b5d31934115f51a3a8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 3 May 2025 02:31:15 +0000 Subject: [PATCH 02/71] Update pre-commit hook astral-sh/ruff-pre-commit to v0.11.8 (#744) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e804df1bb..5fa227bba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_language_version: repos: # Run manually in CI skipping the branch checks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.7 + rev: v0.11.8 hooks: - id: ruff name: "Ruff check" From 0b4bd32e25acb4466cea2f6f8dbd8864904f7de3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 03:26:24 +0000 Subject: [PATCH 03/71] Update pre-commit hook adrienverge/yamllint to v1.37.1 (#745) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5fa227bba..159344d3e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(plugwise|tests)/.+\.py$ - repo: https://github.com/adrienverge/yamllint.git - rev: v1.37.0 + rev: v1.37.1 hooks: - id: yamllint name: "YAML linting" From e2dd8ebd8a98e3314696b822fbf2c2b7fa300c97 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 May 2025 01:49:47 +0000 Subject: [PATCH 04/71] Update pre-commit hook astral-sh/ruff-pre-commit to v0.11.9 (#746) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 159344d3e..0321a5eb8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_language_version: repos: # Run manually in CI skipping the branch checks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.8 + rev: v0.11.9 hooks: - id: ruff name: "Ruff check" From 2ebeb98d854702a983b119c40561ea02ef5b68c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 02:59:39 +0000 Subject: [PATCH 05/71] Update pre-commit hook astral-sh/ruff-pre-commit to v0.11.10 (#747) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0321a5eb8..136dbafcf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_language_version: repos: # Run manually in CI skipping the branch checks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.9 + rev: v0.11.10 hooks: - id: ruff name: "Ruff check" From 1f6a04c5fc7a9c8fbf5d6359917ebb41d6eeb49e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 03:53:34 +0000 Subject: [PATCH 06/71] Update pre-commit hook igorshubovych/markdownlint-cli to v0.45.0 (#748) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 136dbafcf..97d0920d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -109,6 +109,6 @@ repos: entry: ./tmp/biome check fixtures/ plugwise/ tests/ --files-ignore-unknown=true --no-errors-on-unmatched --json-formatter-indent-width=2 --json-formatter-indent-style=space language: script - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.44.0 + rev: v0.45.0 hooks: - id: markdownlint From 60aa2cedbfac739a153bae4c3cafcc69d330d7dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 22:34:59 +0000 Subject: [PATCH 07/71] Update pre-commit hook astral-sh/ruff-pre-commit to v0.11.11 (#749) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97d0920d1..28a6fd630 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_language_version: repos: # Run manually in CI skipping the branch checks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.10 + rev: v0.11.11 hooks: - id: ruff name: "Ruff check" From 21e396c96eb2679a6ce7b99af2abc2613f5336e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 May 2025 01:42:58 +0000 Subject: [PATCH 08/71] Update pre-commit hook asottile/pyupgrade to v3.20.0 (#750) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28a6fd630..38f6b8ee7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: args: - --branch=main - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 + rev: v3.20.0 hooks: - id: pyupgrade name: "Check Py upgrade" From 33772609c908091243117d6521c67aad1dfb4fbd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 05:46:30 +0000 Subject: [PATCH 09/71] Update pre-commit hook astral-sh/ruff-pre-commit to v0.11.12 (#751) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 38f6b8ee7..daf159924 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_language_version: repos: # Run manually in CI skipping the branch checks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.11 + rev: v0.11.12 hooks: - id: ruff name: "Ruff check" From 52610b27f49af11b65a0add9896cd05d03dbffe1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 02:12:47 +0000 Subject: [PATCH 10/71] Update pre-commit hook cdce8p/python-typing-update to v0.7.2 (#752) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index daf159924..bb4e6c7a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -52,7 +52,7 @@ repos: - id: yamllint name: "YAML linting" - repo: https://github.com/cdce8p/python-typing-update - rev: v0.7.1 + rev: v0.7.2 hooks: # Run `python-typing-update` hook manually from time to time # to update python typing syntax. From 966ba34fbb0dab17de6f24ed5c2bd870734b55b1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Jun 2025 06:46:23 +0000 Subject: [PATCH 11/71] Update pre-commit hook astral-sh/ruff-pre-commit to v0.11.13 (#753) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb4e6c7a4..60ee01d63 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_language_version: repos: # Run manually in CI skipping the branch checks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.12 + rev: v0.11.13 hooks: - id: ruff name: "Ruff check" From 72b5c78680192e3c4b7af39fca1995b982670a06 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Fri, 13 Jun 2025 10:01:57 +0200 Subject: [PATCH 12/71] Move codespell config to pre-commit --- .pre-commit-config.yaml | 7 +++++-- pyproject.toml | 6 ++---- setup.cfg | 8 -------- 3 files changed, 7 insertions(+), 14 deletions(-) delete mode 100644 setup.cfg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 60ee01d63..847bf1b4b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,13 +29,16 @@ repos: - id: pyupgrade name: "Check Py upgrade" args: [--py311-plus] - # Moved codespell configuration to setup.cfg as per 'all-files' issues not reading args - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: - id: codespell - name: "Check spelling" + args: + - --ignore-words-list=aiport,astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn,leeg # HA-core copied, self-added: leeg (Dutch for empty) + - --skip="./.*,*.csv,*.json,*.ambr" + - --quiet-level=2 exclude_types: [csv, json] + exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/ - repo: https://github.com/PyCQA/bandit rev: 1.8.3 hooks: diff --git a/pyproject.toml b/pyproject.toml index a4ee2fb8c..7609922e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Home Automation", ] @@ -24,7 +23,7 @@ maintainers = [ { name = "bouwew"}, { name = "CoMPaTech" } ] -requires-python = ">=3.12.0" +requires-python = ">=3.13" dependencies = [ "aiohttp", "defusedxml", @@ -52,7 +51,7 @@ include = ["plugwise*"] # 20241208: W0201 / attribute-defined-outside-init # 20241208: R1702 / too-many-nested-blocks # too many nested blocks in test_init 8/5 # 20241208: R6102 / consider-using-tuple -# 20241208: Recommended disabling => "implicit-str-concat", # ISC001 - 2 occurances! +# 20241208: Recommended disabling => "implicit-str-concat", # ISC001 - 2 occurrences! ## [tool.pylint.MAIN] @@ -466,7 +465,6 @@ lint.select = [ "S317", # suspicious-xml-sax-usage "S318", # suspicious-xml-mini-dom-usage "S319", # suspicious-xml-pull-dom-usage - "S320", # suspicious-xmle-tree-usage "S601", # paramiko-call "S602", # subprocess-popen-with-shell-equals-true "S604", # call-with-shell-equals-true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2c3d87f13..000000000 --- a/setup.cfg +++ /dev/null @@ -1,8 +0,0 @@ -# Added Codespell since pre-commit doesn't process args correctly (and python3.11 and toml prevent using pyproject.toml) check #277 (/#278) for details - -[codespell] -# Most of the ignores from HA-Core upstream -# Self-added: leeg -ignore-words-list = additionals,alle,alot,ba,bre,bund,currenty,datas,dof,dur,ether,farenheit,falsy,fo,haa,hass,hist,iam,iff,iif,incomfort,ines,ist,leeg,lightsensor,mut,nam,nd,pres,pullrequests,referer,resset,rime,ser,serie,sur,te,technik,ue,uint,unsecure,visability,wan,wanna,withing,zar -skip = ./.*,*.csv,*.json -quiet-level = 2 From f65c55b7587a7b95009875421753b946c8ea1b0f Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Fri, 13 Jun 2025 10:02:45 +0200 Subject: [PATCH 13/71] Update scripts wrt uv and venv --- scripts/complexity.sh | 29 +++--- scripts/manual_fixtures.py | 176 ++++++++++++---------------------- scripts/run-in-env.sh | 38 +++++--- scripts/setup.sh | 35 +++---- scripts/setup_test.sh | 69 ++++++------- scripts/tests_and_coverage.sh | 47 +++++---- 6 files changed, 188 insertions(+), 206 deletions(-) diff --git a/scripts/complexity.sh b/scripts/complexity.sh index fbae27090..61b2a6668 100755 --- a/scripts/complexity.sh +++ b/scripts/complexity.sh @@ -3,18 +3,21 @@ set -eu my_path=$(git rev-parse --show-toplevel) -# shellcheck disable=SC1091 -source "${my_path}/scripts/python-venv.sh" - -# shellcheck disable=SC2154 -if [ -f "${my_venv}/bin/activate" ]; then - # shellcheck disable=SC1091 - . "${my_venv}/bin/activate" - echo "-----------------------------" - echo "Running cyclomatic complexity" - echo "-----------------------------" - PYTHONPATH=$(pwd) radon cc plugwise/ tests/ -s -nc --no-assert +if [ -n "${VIRTUAL_ENV-}" ] && [ -f "${VIRTUAL_ENV}/bin/activate" ]; then + . "${VIRTUAL_ENV}/bin/activate" else - echo "Virtualenv available, bailing out" - exit 2 + # other common virtualenvs + my_path=$(git rev-parse --show-toplevel) + + for venv in venv .venv .; do + if [ -f "${my_path}/${venv}/bin/activate" ]; then + . "${my_path}/${venv}/bin/activate" + break + fi + done fi + +echo "-----------------------------" +echo "Running cyclomatic complexity" +echo "-----------------------------" +PYTHONPATH=$(pwd) radon cc plugwise/ tests/ -s -nc --no-assert diff --git a/scripts/manual_fixtures.py b/scripts/manual_fixtures.py index 8cdc4fb24..aae0a2cb9 100755 --- a/scripts/manual_fixtures.py +++ b/scripts/manual_fixtures.py @@ -36,8 +36,12 @@ def json_writer(manual_name: str, output: dict) -> None: adam_multiple_devices_per_zone = base.copy() # Change schedule to not present for "446ac08dd04d4eff8ac57489757b7314" -adam_multiple_devices_per_zone["446ac08dd04d4eff8ac57489757b7314"].pop("available_schedules") -adam_multiple_devices_per_zone["446ac08dd04d4eff8ac57489757b7314"].pop("select_schedule") +adam_multiple_devices_per_zone["446ac08dd04d4eff8ac57489757b7314"].pop( + "available_schedules" +) +adam_multiple_devices_per_zone["446ac08dd04d4eff8ac57489757b7314"].pop( + "select_schedule" +) json_writer("m_adam_multiple_devices_per_zone", adam_multiple_devices_per_zone) @@ -73,24 +77,12 @@ def json_writer(manual_name: str, output: dict) -> None: m_adam_cooling.pop("10016900610d4c7481df78c89606ef22") # Correct setpoint for device "ad4838d7d35c4d6ea796ee12ae5aedf8" and zone "f2bf9048bef64cc5b6d5110154e33c81" -m_adam_cooling["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"][ - "setpoint" -] = 23.5 -m_adam_cooling["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"][ - "temperature" -] = 25.8 -m_adam_cooling["f2bf9048bef64cc5b6d5110154e33c81"]["thermostat"][ - "setpoint" -] = 23.5 -m_adam_cooling["f2bf9048bef64cc5b6d5110154e33c81"]["sensors"][ - "temperature" -] = 25.8 -m_adam_cooling["f2bf9048bef64cc5b6d5110154e33c81"][ - "select_schedule" -] = "off" -m_adam_cooling["f2bf9048bef64cc5b6d5110154e33c81"][ - "control_state" -] = "cooling" +m_adam_cooling["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"]["setpoint"] = 23.5 +m_adam_cooling["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"]["temperature"] = 25.8 +m_adam_cooling["f2bf9048bef64cc5b6d5110154e33c81"]["thermostat"]["setpoint"] = 23.5 +m_adam_cooling["f2bf9048bef64cc5b6d5110154e33c81"]["sensors"]["temperature"] = 25.8 +m_adam_cooling["f2bf9048bef64cc5b6d5110154e33c81"]["select_schedule"] = "off" +m_adam_cooling["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = "cooling" m_adam_cooling["f2bf9048bef64cc5b6d5110154e33c81"]["climate_mode"] = "cool" # Add new key available @@ -105,39 +97,23 @@ def json_writer(manual_name: str, output: dict) -> None: m_adam_cooling.pop("854f8a9b0e7e425db97f1f110e1ce4b3") # Go for 1772 -m_adam_cooling["1772a4ea304041adb83f357b751341ff"]["sensors"][ - "temperature" -] = 21.6 +m_adam_cooling["1772a4ea304041adb83f357b751341ff"]["sensors"]["temperature"] = 21.6 # Go for e2f4 -m_adam_cooling["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"][ - "setpoint" -] = 23.5 -m_adam_cooling["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"][ - "temperature" -] = 23.9 -m_adam_cooling["f871b8c4d63549319221e294e4f88074"]["thermostat"][ - "setpoint" -] = 25.0 -m_adam_cooling["f871b8c4d63549319221e294e4f88074"]["sensors"][ - "temperature" -] = 23.9 -m_adam_cooling["f871b8c4d63549319221e294e4f88074"][ - "control_state" -] = "cooling" +m_adam_cooling["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"]["setpoint"] = 23.5 +m_adam_cooling["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"]["temperature"] = 23.9 +m_adam_cooling["f871b8c4d63549319221e294e4f88074"]["thermostat"]["setpoint"] = 25.0 +m_adam_cooling["f871b8c4d63549319221e294e4f88074"]["sensors"]["temperature"] = 23.9 +m_adam_cooling["f871b8c4d63549319221e294e4f88074"]["control_state"] = "cooling" m_adam_cooling["f871b8c4d63549319221e294e4f88074"]["climate_mode"] = "auto" # Go for da22 -m_adam_cooling["da224107914542988a88561b4452b0f6"][ - "select_regulation_mode" -] = "cooling" -m_adam_cooling["da224107914542988a88561b4452b0f6"][ - "regulation_modes" -].append("cooling") -m_adam_cooling["da224107914542988a88561b4452b0f6"]["sensors"][ - "outdoor_temperature" -] = 29.65 +m_adam_cooling["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = "cooling" +m_adam_cooling["da224107914542988a88561b4452b0f6"]["regulation_modes"].append("cooling") +m_adam_cooling["da224107914542988a88561b4452b0f6"]["sensors"]["outdoor_temperature"] = ( + 29.65 +) # Go for 056e m_adam_cooling["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ @@ -146,12 +122,12 @@ def json_writer(manual_name: str, output: dict) -> None: m_adam_cooling["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ "heating_state" ] = False -m_adam_cooling["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ - "flame_state" -] = False -m_adam_cooling["056ee145a816487eaa69243c3280f8bf"]["sensors"][ - "water_temperature" -] = 19.0 +m_adam_cooling["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["flame_state"] = ( + False +) +m_adam_cooling["056ee145a816487eaa69243c3280f8bf"]["sensors"]["water_temperature"] = ( + 19.0 +) m_adam_cooling["056ee145a816487eaa69243c3280f8bf"]["sensors"][ "intended_boiler_temperature" ] = 17.5 @@ -163,56 +139,30 @@ def json_writer(manual_name: str, output: dict) -> None: m_adam_heating = m_adam_cooling.copy() # Correct setpoint for "ad4838d7d35c4d6ea796ee12ae5aedf8" -m_adam_heating["f2bf9048bef64cc5b6d5110154e33c81"]["thermostat"][ - "setpoint" -] = 20.0 -m_adam_heating["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"][ - "setpoint" -] = 20.0 -m_adam_heating["f2bf9048bef64cc5b6d5110154e33c81"]["sensors"][ - "temperature" -] = 19.1 -m_adam_heating["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"][ - "temperature" -] = 19.1 +m_adam_heating["f2bf9048bef64cc5b6d5110154e33c81"]["thermostat"]["setpoint"] = 20.0 +m_adam_heating["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"]["setpoint"] = 20.0 +m_adam_heating["f2bf9048bef64cc5b6d5110154e33c81"]["sensors"]["temperature"] = 19.1 +m_adam_heating["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"]["temperature"] = 19.1 -m_adam_heating["f2bf9048bef64cc5b6d5110154e33c81"][ - "control_state" -] = "preheating" +m_adam_heating["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = "preheating" m_adam_heating["f2bf9048bef64cc5b6d5110154e33c81"]["climate_mode"] = "heat" # Go for 1772 -m_adam_heating["1772a4ea304041adb83f357b751341ff"]["sensors"][ - "temperature" -] = 18.6 +m_adam_heating["1772a4ea304041adb83f357b751341ff"]["sensors"]["temperature"] = 18.6 # Related zone temperature is set below # Go for e2f4 -m_adam_heating["f871b8c4d63549319221e294e4f88074"]["thermostat"][ - "setpoint" -] = 15.0 -m_adam_heating["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"][ - "setpoint" -] = 15.0 -m_adam_heating["f871b8c4d63549319221e294e4f88074"]["sensors"][ - "temperature" -] = 17.9 -m_adam_heating["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"][ - "temperature" -] = 17.9 +m_adam_heating["f871b8c4d63549319221e294e4f88074"]["thermostat"]["setpoint"] = 15.0 +m_adam_heating["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"]["setpoint"] = 15.0 +m_adam_heating["f871b8c4d63549319221e294e4f88074"]["sensors"]["temperature"] = 17.9 +m_adam_heating["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"]["temperature"] = 17.9 m_adam_heating["f871b8c4d63549319221e294e4f88074"]["climate_mode"] = "auto" -m_adam_heating["f871b8c4d63549319221e294e4f88074"][ - "control_state" -] = "idle" +m_adam_heating["f871b8c4d63549319221e294e4f88074"]["control_state"] = "idle" # Go for da22 -m_adam_heating["da224107914542988a88561b4452b0f6"][ - "select_regulation_mode" -] = "heating" -m_adam_heating["da224107914542988a88561b4452b0f6"][ - "regulation_modes" -].remove("cooling") +m_adam_heating["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = "heating" +m_adam_heating["da224107914542988a88561b4452b0f6"]["regulation_modes"].remove("cooling") m_adam_heating["da224107914542988a88561b4452b0f6"]["sensors"][ "outdoor_temperature" ] = -1.25 @@ -224,12 +174,12 @@ def json_writer(manual_name: str, output: dict) -> None: m_adam_heating["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ "heating_state" ] = True -m_adam_heating["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ - "flame_state" -] = False -m_adam_heating["056ee145a816487eaa69243c3280f8bf"]["sensors"][ - "water_temperature" -] = 37.0 +m_adam_heating["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["flame_state"] = ( + False +) +m_adam_heating["056ee145a816487eaa69243c3280f8bf"]["sensors"]["water_temperature"] = ( + 37.0 +) m_adam_heating["056ee145a816487eaa69243c3280f8bf"]["sensors"][ "intended_boiler_temperature" ] = 38.1 @@ -252,18 +202,18 @@ def json_writer(manual_name: str, output: dict) -> None: m_anna_heatpump_cooling = base.copy() # Go for 1cbf -m_anna_heatpump_cooling["1cbf783bb11e4a7c8a6843dee3a86927"][ - "model" -] = "Generic heater/cooler" -m_anna_heatpump_cooling["1cbf783bb11e4a7c8a6843dee3a86927"][ - "binary_sensors" -]["cooling_enabled"] = True -m_anna_heatpump_cooling["1cbf783bb11e4a7c8a6843dee3a86927"][ - "binary_sensors" -]["heating_state"] = False -m_anna_heatpump_cooling["1cbf783bb11e4a7c8a6843dee3a86927"][ - "binary_sensors" -]["cooling_state"] = True +m_anna_heatpump_cooling["1cbf783bb11e4a7c8a6843dee3a86927"]["model"] = ( + "Generic heater/cooler" +) +m_anna_heatpump_cooling["1cbf783bb11e4a7c8a6843dee3a86927"]["binary_sensors"][ + "cooling_enabled" +] = True +m_anna_heatpump_cooling["1cbf783bb11e4a7c8a6843dee3a86927"]["binary_sensors"][ + "heating_state" +] = False +m_anna_heatpump_cooling["1cbf783bb11e4a7c8a6843dee3a86927"]["binary_sensors"][ + "cooling_state" +] = True m_anna_heatpump_cooling["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ "water_temperature" @@ -344,9 +294,9 @@ def json_writer(manual_name: str, output: dict) -> None: # Go for 3cb7 m_anna_heatpump_idle["3cb70739631c4d17a86b8b12e8a5161b"]["control_state"] = "idle" -m_anna_heatpump_idle["3cb70739631c4d17a86b8b12e8a5161b"]["sensors"][ - "temperature" -] = 23.0 +m_anna_heatpump_idle["3cb70739631c4d17a86b8b12e8a5161b"]["sensors"]["temperature"] = ( + 23.0 +) m_anna_heatpump_idle["3cb70739631c4d17a86b8b12e8a5161b"]["sensors"][ "cooling_activation_outdoor_temperature" ] = 25.0 diff --git a/scripts/run-in-env.sh b/scripts/run-in-env.sh index 624d5ea95..51bc719b8 100755 --- a/scripts/run-in-env.sh +++ b/scripts/run-in-env.sh @@ -1,19 +1,31 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh +# 20250613 Copied from HA-Core (unchanged) set -eu -my_path=$(git rev-parse --show-toplevel) +# Used in venv activate script. +# Would be an error if undefined. +OSTYPE="${OSTYPE-}" -# shellcheck disable=SC1091 -. "${my_path}/scripts/python-venv.sh" +# Activate pyenv and virtualenv if present, then run the specified command -# shellcheck disable=SC2154 -if [ -f "${my_venv}/bin/activate" ]; then - set +o nounset # Workaround https://github.com/pypa/virtualenv/issues/150 for nodeenv - # shellcheck disable=SC1091 - . "${my_venv}/bin/activate" - set -o nounset - exec "$@" +# pyenv, pyenv-virtualenv +if [ -s .python-version ]; then + PYENV_VERSION=$(head -n 1 .python-version) + export PYENV_VERSION +fi + +if [ -n "${VIRTUAL_ENV-}" ] && [ -f "${VIRTUAL_ENV}/bin/activate" ]; then + . "${VIRTUAL_ENV}/bin/activate" else - echo "Virtualenv available, bailing out" - exit 2 + # other common virtualenvs + my_path=$(git rev-parse --show-toplevel) + + for venv in venv .venv .; do + if [ -f "${my_path}/${venv}/bin/activate" ]; then + . "${my_path}/${venv}/bin/activate" + break + fi + done fi + +exec "$@" diff --git a/scripts/setup.sh b/scripts/setup.sh index add58c35b..080053ba4 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -1,23 +1,24 @@ #!/usr/bin/env bash -set -eu +# 20250613 Copied from HA-core and adjusted +set -e my_path=$(git rev-parse --show-toplevel) -# shellcheck disable=SC1091 -. "${my_path}/scripts/python-venv.sh" +if [ ! -n "$VIRTUAL_ENV" ]; then + if [ -x "$(command -v uv)" ]; then + uv venv venv + else + python3 -m venv venv + fi + source venv/bin/activate +fi -# shellcheck disable=SC2154 -if [ -f "${my_venv}/bin/activate" ]; then - set +o nounset # Workaround https://github.com/pypa/virtualenv/issues/150 for nodeenv - # shellcheck disable=SC1091 - . "${my_venv}/bin/activate" - set -o nounset - # Install commit requirements - pip install wheel uv - uv pip install --upgrade -e . -r requirements_commit.txt -c https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/package_constraints.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test_pre_commit.txt - # Install pre-commit hook - "${my_venv}/bin/pre-commit" install -else - echo "Virtualenv available, bailing out" - exit 2 +if ! [ -x "$(command -v uv)" ]; then + python3 -m pip install uv fi + +# Install commit requirements +uv pip install --upgrade -e . -r requirements_commit.txt -c https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/package_constraints.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test_pre_commit.txt + +# Install pre-commit hook +pre-commit install diff --git a/scripts/setup_test.sh b/scripts/setup_test.sh index a117c7011..d20f8075c 100755 --- a/scripts/setup_test.sh +++ b/scripts/setup_test.sh @@ -1,39 +1,42 @@ #!/usr/bin/env bash -set -eu +# 20250613 Copied from HA-core and adjusted +set -e my_path=$(git rev-parse --show-toplevel) -# shellcheck disable=SC1091 -. "${my_path}/scripts/python-venv.sh" - -# shellcheck disable=SC2154 -if [ -f "${my_venv}/bin/activate" ]; then - set +o nounset # Workaround https://github.com/pypa/virtualenv/issues/150 for nodeenv - # shellcheck disable=SC1091 - . "${my_venv}/bin/activate" - mkdir -p ./tmp - set -o nounset - # Install test requirements - uv pip install --upgrade -e . -r requirements_test.txt -c https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/package_constraints.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test_pre_commit.txt - # Prepare biomejs - echo "Fetching/updating biome cli" - if uname -a | grep -q arm64; then - curl -sL "https://github.com/biomejs/biome/releases/latest/download/biome-darwin-arm64" -o "${my_path}/tmp/biome" - elif uname -a | grep -q x86_64; then - curl -sL "https://github.com/biomejs/biome/releases/latest/download/biome-linux-x64" -o "${my_path}/tmp/biome" - else - echo "Unable to determine processor and as such to install packaged biome cli version, bailing out" - exit 2 - fi - - # Make biome executable (if necessary) - chmod +x "${my_path}/tmp/biome" - - # Install pre-commit hook unless running from within pre-commit - if [ "$#" -eq 0 ]; then - "${my_venv}/bin/pre-commit" install - fi +if [ ! -n "$VIRTUAL_ENV" ]; then + if [ -x "$(command -v uv)" ]; then + uv venv venv + else + python3 -m venv venv + fi + source venv/bin/activate +fi + +if ! [ -x "$(command -v uv)" ]; then + python3 -m pip install uv +fi + +mkdir -p ./tmp + +# Install test requirements +uv pip install --upgrade -e . -r requirements_test.txt -c https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/package_constraints.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test_pre_commit.txt + +# Prepare biomejs +echo "Fetching/updating biome cli" +if uname -a | grep -q arm64; then + curl -sL "https://github.com/biomejs/biome/releases/latest/download/biome-darwin-arm64" -o "${my_path}/tmp/biome" +elif uname -a | grep -q x86_64; then + curl -sL "https://github.com/biomejs/biome/releases/latest/download/biome-linux-x64" -o "${my_path}/tmp/biome" else - echo "Virtualenv available, bailing out" - exit 2 + echo "Unable to determine processor and as such to install packaged biome cli version, bailing out" + exit 2 +fi + +# Make biome executable (if necessary) +chmod +x "${my_path}/tmp/biome" + +# Install pre-commit hook unless running from within pre-commit +if [ "$#" -eq 0 ]; then + pre-commit install fi diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 73be7e825..522b80da5 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -1,24 +1,38 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh +# 20250613 Copied from HA-Core: run-in-env.sh set -eu -my_path=$(git rev-parse --show-toplevel) +# Used in venv activate script. +# Would be an error if undefined. +OSTYPE="${OSTYPE-}" -# shellcheck disable=SC1091 -. "${my_path}/scripts/python-venv.sh" +# Activate pyenv and virtualenv if present, then run the specified command -# shellcheck disable=SC2154 -if [ -f "${my_venv}/bin/activate" ]; then - set +o nounset # Workaround https://github.com/pypa/virtualenv/issues/150 for nodeenv - # shellcheck disable=SC1091 - . "${my_venv}/bin/activate" - set -o nounset - if [ ! "$(which pytest)" ]; then - echo "Unable to find pytest, run setup_test.sh before this script" - exit 1 - fi +# pyenv, pyenv-virtualenv +if [ -s .python-version ]; then + PYENV_VERSION=$(head -n 1 .python-version) + export PYENV_VERSION +fi + +if [ -n "${VIRTUAL_ENV-}" ] && [ -f "${VIRTUAL_ENV}/bin/activate" ]; then + . "${VIRTUAL_ENV}/bin/activate" else - echo "Virtualenv available, bailing out" - exit 2 + # other common virtualenvs + my_path=$(git rev-parse --show-toplevel) + + for venv in venv .venv .; do + if [ -f "${my_path}/${venv}/bin/activate" ]; then + . "${my_path}/${venv}/bin/activate" + break + fi + done +fi + +# 20250613 End of copy + +if [ ! "$(which pytest)" ]; then + echo "Unable to find pytest, run setup_test.sh before this script" + exit 1 fi handle_command_error() { @@ -36,7 +50,6 @@ biome_format() { # Install/update dependencies pre-commit install pre-commit install-hooks -pip install uv uv pip install -r requirements_test.txt -r requirements_commit.txt set +u From cd3b820d577b3990eb49a9cb76f165e8d8c7c0e4 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Fri, 13 Jun 2025 10:03:17 +0200 Subject: [PATCH 14/71] Redundant script --- scripts/python-venv.sh | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100755 scripts/python-venv.sh diff --git a/scripts/python-venv.sh b/scripts/python-venv.sh deleted file mode 100755 index bef499779..000000000 --- a/scripts/python-venv.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -set -eu - -pyversions=(3.13 3.12) -my_path=$(git rev-parse --show-toplevel) -my_venv=${my_path}/venv - -# Ensures a python virtualenv is available at the highest available python3 version -for pv in "${pyversions[@]}"; do - if [ "$(which "python$pv")" ]; then - # If not (yet) available instantiate python virtualenv - if [ ! -d "${my_venv}" ]; then - "python${pv}" -m venv "${my_venv}" - # Ensure wheel is installed (preventing local issues) - # shellcheck disable=SC1091 - . "${my_venv}/bin/activate" - pip install wheel - fi - break - fi -done - -# Failsafe -if [ ! -d "${my_venv}" ]; then - echo "Unable to instantiate venv, check your base python3 version and if you have python3-venv installed" - exit 1 -fi From 59de1dbc49ca70ef4a98e681136a5857ebf4d286 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Fri, 13 Jun 2025 10:13:37 +0200 Subject: [PATCH 15/71] Improve/clean-up codespell --- .pre-commit-config.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 847bf1b4b..0c57ff422 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,12 +33,13 @@ repos: rev: v2.4.1 hooks: - id: codespell + name: "Check Code Spelling" args: - - --ignore-words-list=aiport,astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn,leeg # HA-core copied, self-added: leeg (Dutch for empty) + - --ignore-words-list=aiport,astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn - --skip="./.*,*.csv,*.json,*.ambr" - --quiet-level=2 exclude_types: [csv, json] - exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/ + exclude: ^userdata/|^fixtures/ - repo: https://github.com/PyCQA/bandit rev: 1.8.3 hooks: From 5cc7afc4b1aa24e69e4ab883f3331a428d3c138c Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Fri, 13 Jun 2025 10:32:49 +0200 Subject: [PATCH 16/71] Shellcheck --- .pre-commit-config.yaml | 7 +++++++ scripts/complexity.sh | 2 ++ scripts/run-in-env.sh | 2 ++ scripts/setup.sh | 6 ++---- scripts/setup_test.sh | 4 ++-- scripts/tests_and_coverage.sh | 4 +++- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c57ff422..917243a26 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,6 +55,13 @@ repos: hooks: - id: yamllint name: "YAML linting" + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.10.0.1 + hooks: + - id: shellcheck + name: "Shell checking" + args: + - --external-sources - repo: https://github.com/cdce8p/python-typing-update rev: v0.7.2 hooks: diff --git a/scripts/complexity.sh b/scripts/complexity.sh index 61b2a6668..8089723c9 100755 --- a/scripts/complexity.sh +++ b/scripts/complexity.sh @@ -4,6 +4,7 @@ set -eu my_path=$(git rev-parse --show-toplevel) if [ -n "${VIRTUAL_ENV-}" ] && [ -f "${VIRTUAL_ENV}/bin/activate" ]; then + # shellcheck disable=SC1091 # ingesting virtualenv . "${VIRTUAL_ENV}/bin/activate" else # other common virtualenvs @@ -11,6 +12,7 @@ else for venv in venv .venv .; do if [ -f "${my_path}/${venv}/bin/activate" ]; then + # shellcheck disable=SC1090 # ingesting virtualenv . "${my_path}/${venv}/bin/activate" break fi diff --git a/scripts/run-in-env.sh b/scripts/run-in-env.sh index 51bc719b8..946cbb3e5 100755 --- a/scripts/run-in-env.sh +++ b/scripts/run-in-env.sh @@ -15,6 +15,7 @@ if [ -s .python-version ]; then fi if [ -n "${VIRTUAL_ENV-}" ] && [ -f "${VIRTUAL_ENV}/bin/activate" ]; then + # shellcheck disable=SC1091 # ingesting virtualenv . "${VIRTUAL_ENV}/bin/activate" else # other common virtualenvs @@ -22,6 +23,7 @@ else for venv in venv .venv .; do if [ -f "${my_path}/${venv}/bin/activate" ]; then + # shellcheck disable=SC1090 # ingesting virtualenv . "${my_path}/${venv}/bin/activate" break fi diff --git a/scripts/setup.sh b/scripts/setup.sh index 080053ba4..19507f603 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -1,10 +1,8 @@ #!/usr/bin/env bash -# 20250613 Copied from HA-core and adjusted +# 20250613 Copied from HA-core and shell-check adjusted and modified for local use set -e -my_path=$(git rev-parse --show-toplevel) - -if [ ! -n "$VIRTUAL_ENV" ]; then +if [ -z "$VIRTUAL_ENV" ]; then if [ -x "$(command -v uv)" ]; then uv venv venv else diff --git a/scripts/setup_test.sh b/scripts/setup_test.sh index d20f8075c..80e0b08a6 100755 --- a/scripts/setup_test.sh +++ b/scripts/setup_test.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# 20250613 Copied from HA-core and adjusted +# 20250613 Copied from HA-core and shell-check adjusted and modified for local use set -e my_path=$(git rev-parse --show-toplevel) -if [ ! -n "$VIRTUAL_ENV" ]; then +if [ -z "$VIRTUAL_ENV" ]; then if [ -x "$(command -v uv)" ]; then uv venv venv else diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 522b80da5..7976a075c 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash # 20250613 Copied from HA-Core: run-in-env.sh set -eu @@ -15,6 +15,7 @@ if [ -s .python-version ]; then fi if [ -n "${VIRTUAL_ENV-}" ] && [ -f "${VIRTUAL_ENV}/bin/activate" ]; then + # shellcheck disable=SC1091 # ingesting virtualenv . "${VIRTUAL_ENV}/bin/activate" else # other common virtualenvs @@ -22,6 +23,7 @@ else for venv in venv .venv .; do if [ -f "${my_path}/${venv}/bin/activate" ]; then + # shellcheck disable=SC1090 # ingesting virtualenv . "${my_path}/${venv}/bin/activate" break fi From 245c9a4a6d9cf383b542fda94010036916966123 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Fri, 13 Jun 2025 10:37:27 +0200 Subject: [PATCH 17/71] Suggested improvements --- scripts/tests_and_coverage.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 7976a075c..f4a1b0742 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -32,7 +32,7 @@ fi # 20250613 End of copy -if [ ! "$(which pytest)" ]; then +if ! command -v pytest >/dev/null; then echo "Unable to find pytest, run setup_test.sh before this script" exit 1 fi From ece7c26ad1bc876c73bef83eac9b93c9738e1628 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Fri, 13 Jun 2025 10:47:14 +0200 Subject: [PATCH 18/71] Further ingests --- scripts/setup.sh | 1 + scripts/setup_test.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/setup.sh b/scripts/setup.sh index 19507f603..00073f8d6 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -8,6 +8,7 @@ if [ -z "$VIRTUAL_ENV" ]; then else python3 -m venv venv fi + # shellcheck disable=SC1091 # ingesting virtualenv source venv/bin/activate fi diff --git a/scripts/setup_test.sh b/scripts/setup_test.sh index 80e0b08a6..589cb0a2b 100755 --- a/scripts/setup_test.sh +++ b/scripts/setup_test.sh @@ -10,6 +10,7 @@ if [ -z "$VIRTUAL_ENV" ]; then else python3 -m venv venv fi + # shellcheck disable=SC1091 # ingesting virtualenv source venv/bin/activate fi From db71ec633fe87858741f5632c580d73ecebaf262 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Fri, 13 Jun 2025 11:06:47 +0200 Subject: [PATCH 19/71] Bump version and deprecate accordingly --- .github/workflows/verify.yml | 6 +++--- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 984d90570..186d259e7 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -4,7 +4,7 @@ name: Latest commit env: - CACHE_VERSION: 12 + CACHE_VERSION: 13 DEFAULT_PYTHON: "3.13" PRE_COMMIT_HOME: ~/.cache/pre-commit @@ -173,7 +173,7 @@ jobs: needs: commitcheck strategy: matrix: - python-version: ["3.13", "3.12"] + python-version: ["3.13"] steps: - name: Check out committed code uses: actions/checkout@v4 @@ -213,7 +213,7 @@ jobs: needs: prepare-test-cache strategy: matrix: - python-version: ["3.13", "3.12"] + python-version: ["3.13"] steps: - name: Check out committed code diff --git a/CHANGELOG.md b/CHANGELOG.md index 660a53a25..4518ae709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v1.7.5 + +- Maintenance chores +- Deprecating python 3.12 + +## v1.7.4 + +- Maintenance chores + ## v1.7.3 - Improve readability of xml-data in POST/PUT requests via [#707](https://github.com/plugwise/python-plugwise/pull/707), [#708](https://github.com/plugwise/python-plugwise/pull/708) and [#715](https://github.com/plugwise/python-plugwise/pull/715) diff --git a/pyproject.toml b/pyproject.toml index 7609922e4..a2dd35387 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise" -version = "1.7.4" +version = "1.7.5" license = "MIT" description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3." readme = "README.md" From 063d67e2e0f11485564af81da07bb3ca5a6f54f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 01:13:13 +0000 Subject: [PATCH 20/71] Update pre-commit hook PyCQA/bandit to v1.8.4 (#756) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 917243a26..293dc569c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: exclude_types: [csv, json] exclude: ^userdata/|^fixtures/ - repo: https://github.com/PyCQA/bandit - rev: 1.8.3 + rev: 1.8.4 hooks: - id: bandit name: "Bandit checking" From c52bf8b3bb74d186983890d75d4217265d780045 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 02:48:51 +0000 Subject: [PATCH 21/71] Update pre-commit hook PyCQA/bandit to v1.8.5 (#757) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 293dc569c..89d3f5cb9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: exclude_types: [csv, json] exclude: ^userdata/|^fixtures/ - repo: https://github.com/PyCQA/bandit - rev: 1.8.4 + rev: 1.8.5 hooks: - id: bandit name: "Bandit checking" From c945853e522e12b3f82471c755fa6b71ab17a865 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 05:42:00 +0000 Subject: [PATCH 22/71] Update pre-commit hook astral-sh/ruff-pre-commit to v0.12.0 (#758) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 89d3f5cb9..9ff73a7b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_language_version: repos: # Run manually in CI skipping the branch checks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.13 + rev: v0.12.0 hooks: - id: ruff name: "Ruff check" From 4c3ebd705ac16802e5abe9dd7c77eaa5b66a4600 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Wed, 18 Jun 2025 22:15:11 +0200 Subject: [PATCH 23/71] Rework/backport changes between USB/network --- .github/actions/restore-venv/action.yml | 60 +++++ .github/workflows/verify.yml | 311 +++++++++--------------- CHANGELOG.md | 4 + requirements_test.txt | 1 + scripts/setup_test.sh | 15 +- 5 files changed, 184 insertions(+), 207 deletions(-) create mode 100644 .github/actions/restore-venv/action.yml diff --git a/.github/actions/restore-venv/action.yml b/.github/actions/restore-venv/action.yml new file mode 100644 index 000000000..0c920a75f --- /dev/null +++ b/.github/actions/restore-venv/action.yml @@ -0,0 +1,60 @@ +name: "Restore venv and pre-commit from cache" +description: "Restores the venv and pre-commit cache or fails" + +inputs: + python-version: + required: true + venv-dir: + required: true + precommit-home: + required: true + cache-key: + required: true + + fail-on-miss: + required: false + default: "true" # DefauLt fail if not available + +runs: + using: "composite" + steps: + - name: Create or reuse cache + id: cache-create + uses: actions/cache@v4 + with: + path: | + ${{ inputs.venv-dir }} + ${{ inputs.precommit-home }} + key: ${{ inputs.cache-key }} + - name: Create Python virtual environment + if: ${{ steps.cache-create.outputs.cache-hit != 'true' }} + shell: bash + run: | + pip install virtualenv --upgrade + python -m venv venv + . venv/bin/activate + pip install uv + uv pip install -U pip setuptools wheel + # 20220124 Mimic setup_test.sh + uv pip install --upgrade -r requirements_commit.txt -r requirements_test.txt -c https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/package_constraints.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test_pre_commit.txt + uv pip install --upgrade pytest-asyncio + - name: Install pre-commit dependencies + if: ${{ steps.cache-create.outputs.cache-hit != 'true' }} + shell: bash + run: | + . venv/bin/activate + pre-commit install-hooks + - name: Save cache if (purposely) created + if: ${{ inputs.fail-on-miss == 'false' && steps.cache-create.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v4 + with: + key: ${{ inputs.cache-key }} + path: | + ${{ inputs.venv-dir }} + ${{ inputs.precommit-home }} + - name: Fail job if Python cache restore failed + if: ${{ inputs.fail-on-miss == 'true' && steps.cache-create.outputs.cache-hit != 'true' }} + shell: bash + run: | + echo "Failed to restore cache for ${{ inputs.python-version}} virtual environment from cache" + exit 1 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 186d259e7..725494c2e 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -4,9 +4,10 @@ name: Latest commit env: - CACHE_VERSION: 13 + CACHE_VERSION: 14 DEFAULT_PYTHON: "3.13" PRE_COMMIT_HOME: ~/.cache/pre-commit + VENV: venv on: schedule: @@ -16,9 +17,28 @@ on: # pull_request: jobs: + # Determine cache key once + cache: + runs-on: ubuntu-latest + name: Cache identify + outputs: + cache-key: ${{ steps.set-key.outputs.cache-key }} + steps: + - name: Check out committed code + uses: actions/checkout@v4 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Compute cache key + id: set-key + run: echo "cache-key=${{ runner.os }}-venv-cache-${{ env.CACHE_VERSION }}-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> "$GITHUB_OUTPUT" + # Prepare default python version environment prepare: runs-on: ubuntu-latest + needs: cache name: Prepare steps: - name: Check out committed code @@ -28,48 +48,22 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python ${{ env.DEFAULT_PYTHON }} virtual environment - id: cache-venv - uses: actions/cache@v4 - with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('setup.py') }} - restore-keys: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('setup.py') }}- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements_test.txt') }} - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- - - name: Create Python virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - pip install virtualenv --upgrade - python -m venv venv - . venv/bin/activate - pip install uv - uv pip install -U pip setuptools wheel - uv pip install -r requirements_test.txt -r requirements_commit.txt - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v4 + - name: Create or reuse cache + id: cache-reuse + uses: ./.github/actions/restore-venv with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit- - - name: Install pre-commit dependencies - if: steps.cache-precommit.outputs.cache-hit != 'true' - run: | - . venv/bin/activate - pre-commit install-hooks + cache-key: ${{ needs.cache.outputs.cache-key }} + fail-on-miss: false # First time create cache (if not already exists) + python-version: ${{ steps.python.outputs.python-version }} + venv-dir: ${{ env.VENV }} + precommit-home: ${{ env.PRE_COMMIT_HOME }} ruff: runs-on: ubuntu-latest name: Ruff check and force - needs: prepare + needs: + - cache + - prepare steps: - name: Check out committed code uses: actions/checkout@v4 @@ -80,21 +74,14 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python ${{ env.DEFAULT_PYTHON }} virtual environment - id: cache-venv - uses: actions/cache@v4 + - name: Create or reuse cache + id: cache-reuse + uses: ./.github/actions/restore-venv with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('setup.py') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python ${{ env.DEFAULT_PYTHON }} virtual environment from cache" - exit 1 + cache-key: ${{ needs.cache.outputs.cache-key }} + python-version: ${{ steps.python.outputs.python-version }} + venv-dir: ${{ env.VENV }} + precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Ruff (with fix) run: | . venv/bin/activate @@ -115,6 +102,8 @@ jobs: runs-on: ubuntu-latest name: Check commit needs: + - cache + - prepare - ruff - shellcheck - dependencies_check @@ -126,33 +115,14 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python ${{ env.DEFAULT_PYTHON }} virtual environment - id: cache-venv - uses: actions/cache@v4 - with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('setup.py') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python ${{ env.DEFAULT_PYTHON }} virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v4 + - name: Create or reuse cache + id: cache-reuse + uses: ./.github/actions/restore-venv with: - path: ${{ env.PRE_COMMIT_HOME }} - key: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore pre-commit environment from cache" - exit 1 + cache-key: ${{ needs.cache.outputs.cache-key }} + python-version: ${{ steps.python.outputs.python-version }} + venv-dir: ${{ env.VENV }} + precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Verify commit run: | . venv/bin/activate @@ -167,54 +137,16 @@ jobs: . venv/bin/activate pre-commit run --show-diff-on-failure --color=always --all-files --hook-stage manual markdownlint - prepare-test-cache: - runs-on: ubuntu-latest - name: Create pytest cache for Python ${{ matrix.python-version }} - needs: commitcheck - strategy: - matrix: - python-version: ["3.13"] - steps: - - name: Check out committed code - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - id: python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache@v4 - with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ - matrix.python-version }}-${{ hashFiles('requirements_test.txt') - }}-${{ hashFiles('setup.py') }} - restore-keys: | - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('setup.py') }} - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }} - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}- - - name: Create full Python ${{ matrix.python-version }} virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - python -m venv venv - . venv/bin/activate - pip install uv - uv pip install -U pip setuptools wheel - #pip install -r requirements_test.txt - # 20220124 Mimic setup_test.sh - uv pip install --upgrade -r requirements_test.txt -c https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/package_constraints.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test_pre_commit.txt - uv pip install --upgrade pytest-asyncio - pytest: runs-on: ubuntu-latest name: Run pytest using Python ${{ matrix.python-version }} - needs: prepare-test-cache + needs: + - cache + - prepare + - commitcheck strategy: matrix: python-version: ["3.13"] - steps: - name: Check out committed code uses: actions/checkout@v4 @@ -223,20 +155,15 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache@v4 + - name: Create or reuse cache + id: cache-reuse + uses: ./.github/actions/restore-venv with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ - matrix.python-version }}-${{ hashFiles('requirements_test.txt') - }}-${{ hashFiles('setup.py') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 + cache-key: ${{ needs.cache.outputs.cache-key }}-pytest-matrix-${{ matrix.python-version }} + fail-on-miss: false # First time create cache (if not already exists) + python-version: ${{ matrix.python-version }} + venv-dir: ${{ env.VENV }} + precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Run all tests run: | . venv/bin/activate @@ -252,7 +179,10 @@ jobs: mypy: runs-on: ubuntu-latest name: Run mypy - needs: pytest + needs: + - cache + - prepare + - pytest steps: - name: Check out committed code uses: actions/checkout@v4 @@ -263,21 +193,14 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python ${{ env.DEFAULT_PYTHON }} virtual environment - id: cache-venv - uses: actions/cache@v4 + - name: Create or reuse cache + id: cache-reuse + uses: ./.github/actions/restore-venv with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('setup.py') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python ${{ env.DEFAULT_PYTHON }} virtual environment from cache" - exit 1 + cache-key: ${{ needs.cache.outputs.cache-key }} + python-version: ${{ steps.python.outputs.python-version }} + venv-dir: ${{ env.VENV }} + precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Run mypy run: | . venv/bin/activate @@ -307,7 +230,11 @@ jobs: coverage: name: Process test coverage runs-on: ubuntu-latest - needs: pytest + needs: + - cache + - prepare + - pytest + - mypy steps: - name: Check out committed code uses: actions/checkout@v4 @@ -316,21 +243,14 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python ${{ env.DEFAULT_PYTHON }} virtual environment - id: cache-venv - uses: actions/cache@v4 + - name: Create or reuse cache + id: cache-reuse + uses: ./.github/actions/restore-venv with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('setup.py') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 + cache-key: ${{ needs.cache.outputs.cache-key }} + python-version: ${{ steps.python.outputs.python-version }} + venv-dir: ${{ env.VENV }} + precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Download all coverage artifacts uses: actions/download-artifact@v4 - name: Combine coverage results @@ -347,7 +267,11 @@ jobs: test-publishing: name: Build and publish Python 🐍 distributions 📦 to TestPyPI runs-on: ubuntu-latest - needs: [coverage, mypy] + needs: + - cache + - prepare + - coverage + - mypy steps: - name: Check out committed code uses: actions/checkout@v4 @@ -356,29 +280,22 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python ${{ env.DEFAULT_PYTHON }} virtual environment - id: cache-venv - uses: actions/cache@v4 + - name: Create or reuse cache + id: cache-reuse + uses: ./.github/actions/restore-venv with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('setup.py') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 + cache-key: ${{ needs.cache.outputs.cache-key }} + python-version: ${{ steps.python.outputs.python-version }} + venv-dir: ${{ env.VENV }} + precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Install pypa/build - run: >- - python3 -m - pip install - build - --user + run: | + . venv/bin/activate + uv pip install build - name: Build a binary wheel and a source tarball - run: python3 -m build + run: | + . venv/bin/activate + python3 -m build - name: Publish distribution 📦 to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 continue-on-error: true @@ -390,7 +307,10 @@ jobs: complexity: name: Process test complexity runs-on: ubuntu-latest - needs: coverage + needs: + - cache + - prepare + - coverage steps: - name: Check out committed code uses: actions/checkout@v4 @@ -399,21 +319,14 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python ${{ env.DEFAULT_PYTHON }} virtual environment - id: cache-venv - uses: actions/cache@v4 + - name: Create or reuse cache + id: cache-reuse + uses: ./.github/actions/restore-venv with: - path: venv - key: >- - ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ - steps.python.outputs.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('setup.py') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 + cache-key: ${{ needs.cache.outputs.cache-key }} + python-version: ${{ steps.python.outputs.python-version }} + venv-dir: ${{ env.VENV }} + precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Run complexity report (click to view details) run: | . venv/bin/activate diff --git a/CHANGELOG.md b/CHANGELOG.md index 4518ae709..d5e02bd4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Ongoing + +- Maintenance chores (mostly reworking Github CI Actions) backporting from efforts on Python Plugwise [USB: 264](https://github.com/plugwise/python-plugwise-usb/pull/264) after porting our progress using [USB: 263](https://github.com/plugwise/python-plugwise-usb/pull/263) + ## v1.7.5 - Maintenance chores diff --git a/requirements_test.txt b/requirements_test.txt index 788f4c116..bd6bb3177 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,3 +5,4 @@ pytest-asyncio radon==6.0.1 types-python-dateutil uv +pytest-cov diff --git a/scripts/setup_test.sh b/scripts/setup_test.sh index 589cb0a2b..2120fe14c 100755 --- a/scripts/setup_test.sh +++ b/scripts/setup_test.sh @@ -25,14 +25,13 @@ uv pip install --upgrade -e . -r requirements_test.txt -c https://raw.githubuser # Prepare biomejs echo "Fetching/updating biome cli" -if uname -a | grep -q arm64; then - curl -sL "https://github.com/biomejs/biome/releases/latest/download/biome-darwin-arm64" -o "${my_path}/tmp/biome" -elif uname -a | grep -q x86_64; then - curl -sL "https://github.com/biomejs/biome/releases/latest/download/biome-linux-x64" -o "${my_path}/tmp/biome" -else - echo "Unable to determine processor and as such to install packaged biome cli version, bailing out" - exit 2 -fi +arch=$(uname -m) +case "$arch" in + aarch64|arm64) use_arch="darwin-arm64" ;; + x86_64) use_arch="linux-x64" ;; + *) echo "Unsupported arch for biome cli version: $arch"; exit 2 ;; +esac +curl -sL "https://github.com/biomejs/biome/releases/latest/download/biome-${use_arch}" -o "${my_path}/tmp/biome" # Make biome executable (if necessary) chmod +x "${my_path}/tmp/biome" From 93ecb44a4f0b6c1680d822b8752505bc977d00a5 Mon Sep 17 00:00:00 2001 From: Tom Scholten Date: Sat, 21 Jun 2025 21:27:57 +0200 Subject: [PATCH 24/71] Migrate actions to plugwise common gh-actions --- .github/actions/restore-venv/action.yml | 60 --------------- .github/workflows/verify.yml | 98 ++++++++----------------- 2 files changed, 29 insertions(+), 129 deletions(-) delete mode 100644 .github/actions/restore-venv/action.yml diff --git a/.github/actions/restore-venv/action.yml b/.github/actions/restore-venv/action.yml deleted file mode 100644 index 0c920a75f..000000000 --- a/.github/actions/restore-venv/action.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: "Restore venv and pre-commit from cache" -description: "Restores the venv and pre-commit cache or fails" - -inputs: - python-version: - required: true - venv-dir: - required: true - precommit-home: - required: true - cache-key: - required: true - - fail-on-miss: - required: false - default: "true" # DefauLt fail if not available - -runs: - using: "composite" - steps: - - name: Create or reuse cache - id: cache-create - uses: actions/cache@v4 - with: - path: | - ${{ inputs.venv-dir }} - ${{ inputs.precommit-home }} - key: ${{ inputs.cache-key }} - - name: Create Python virtual environment - if: ${{ steps.cache-create.outputs.cache-hit != 'true' }} - shell: bash - run: | - pip install virtualenv --upgrade - python -m venv venv - . venv/bin/activate - pip install uv - uv pip install -U pip setuptools wheel - # 20220124 Mimic setup_test.sh - uv pip install --upgrade -r requirements_commit.txt -r requirements_test.txt -c https://raw.githubusercontent.com/home-assistant/core/dev/homeassistant/package_constraints.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test.txt -r https://raw.githubusercontent.com/home-assistant/core/dev/requirements_test_pre_commit.txt - uv pip install --upgrade pytest-asyncio - - name: Install pre-commit dependencies - if: ${{ steps.cache-create.outputs.cache-hit != 'true' }} - shell: bash - run: | - . venv/bin/activate - pre-commit install-hooks - - name: Save cache if (purposely) created - if: ${{ inputs.fail-on-miss == 'false' && steps.cache-create.outputs.cache-hit != 'true' }} - uses: actions/cache/save@v4 - with: - key: ${{ inputs.cache-key }} - path: | - ${{ inputs.venv-dir }} - ${{ inputs.precommit-home }} - - name: Fail job if Python cache restore failed - if: ${{ inputs.fail-on-miss == 'true' && steps.cache-create.outputs.cache-hit != 'true' }} - shell: bash - run: | - echo "Failed to restore cache for ${{ inputs.python-version}} virtual environment from cache" - exit 1 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 725494c2e..4683a4fb4 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -31,6 +31,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.DEFAULT_PYTHON }} + - name: Fetch HA pyproject + id: core-version + run: wget -O ha_pyproject.toml "https://raw.githubusercontent.com/home-assistant/core/refs/heads/dev/pyproject.toml" - name: Compute cache key id: set-key run: echo "cache-key=${{ runner.os }}-venv-cache-${{ env.CACHE_VERSION }}-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> "$GITHUB_OUTPUT" @@ -41,20 +44,13 @@ jobs: needs: cache name: Prepare steps: - - name: Check out committed code - uses: actions/checkout@v4 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Create or reuse cache + - name: Prepare code checkout and python/pre-commit setup id: cache-reuse - uses: ./.github/actions/restore-venv + uses: plugwise/gh-actions/prepare-python-and-code@v1 with: cache-key: ${{ needs.cache.outputs.cache-key }} - fail-on-miss: false # First time create cache (if not already exists) - python-version: ${{ steps.python.outputs.python-version }} + fail-on-miss: false # First time create cache (if not already exists) + python-version: ${{ env.DEFAULT_PYTHON }} venv-dir: ${{ env.VENV }} precommit-home: ${{ env.PRE_COMMIT_HOME }} @@ -69,17 +65,12 @@ jobs: uses: actions/checkout@v4 with: persist-credentials: false - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Create or reuse cache + - name: Restore cached environment id: cache-reuse - uses: ./.github/actions/restore-venv + uses: plugwise/gh-actions/restore-venv@v1 with: cache-key: ${{ needs.cache.outputs.cache-key }} - python-version: ${{ steps.python.outputs.python-version }} + python-version: ${{ env.DEFAULT_PYTHON }} venv-dir: ${{ env.VENV }} precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Ruff (with fix) @@ -110,17 +101,12 @@ jobs: steps: - name: Check out committed code uses: actions/checkout@v4 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Create or reuse cache + - name: Restore cached environment id: cache-reuse - uses: ./.github/actions/restore-venv + uses: plugwise/gh-actions/restore-venv@v1 with: cache-key: ${{ needs.cache.outputs.cache-key }} - python-version: ${{ steps.python.outputs.python-version }} + python-version: ${{ env.DEFAULT_PYTHON }} venv-dir: ${{ env.VENV }} precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Verify commit @@ -150,18 +136,12 @@ jobs: steps: - name: Check out committed code uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - id: python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Create or reuse cache + - name: Restore cached environment id: cache-reuse - uses: ./.github/actions/restore-venv + uses: plugwise/gh-actions/restore-venv@v1 with: - cache-key: ${{ needs.cache.outputs.cache-key }}-pytest-matrix-${{ matrix.python-version }} - fail-on-miss: false # First time create cache (if not already exists) - python-version: ${{ matrix.python-version }} + cache-key: ${{ needs.cache.outputs.cache-key }} + python-version: ${{ env.DEFAULT_PYTHON }} venv-dir: ${{ env.VENV }} precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Run all tests @@ -188,17 +168,12 @@ jobs: uses: actions/checkout@v4 with: persist-credentials: false - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Create or reuse cache + - name: Restore cached environment id: cache-reuse - uses: ./.github/actions/restore-venv + uses: plugwise/gh-actions/restore-venv@v1 with: cache-key: ${{ needs.cache.outputs.cache-key }} - python-version: ${{ steps.python.outputs.python-version }} + python-version: ${{ env.DEFAULT_PYTHON }} venv-dir: ${{ env.VENV }} precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Run mypy @@ -238,17 +213,12 @@ jobs: steps: - name: Check out committed code uses: actions/checkout@v4 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Create or reuse cache + - name: Restore cached environment id: cache-reuse - uses: ./.github/actions/restore-venv + uses: plugwise/gh-actions/restore-venv@v1 with: cache-key: ${{ needs.cache.outputs.cache-key }} - python-version: ${{ steps.python.outputs.python-version }} + python-version: ${{ env.DEFAULT_PYTHON }} venv-dir: ${{ env.VENV }} precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Download all coverage artifacts @@ -275,17 +245,12 @@ jobs: steps: - name: Check out committed code uses: actions/checkout@v4 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Create or reuse cache + - name: Restore cached environment id: cache-reuse - uses: ./.github/actions/restore-venv + uses: plugwise/gh-actions/restore-venv@v1 with: cache-key: ${{ needs.cache.outputs.cache-key }} - python-version: ${{ steps.python.outputs.python-version }} + python-version: ${{ env.DEFAULT_PYTHON }} venv-dir: ${{ env.VENV }} precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Install pypa/build @@ -314,17 +279,12 @@ jobs: steps: - name: Check out committed code uses: actions/checkout@v4 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v5 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Create or reuse cache + - name: Restore cached environment id: cache-reuse - uses: ./.github/actions/restore-venv + uses: plugwise/gh-actions/restore-venv@v1 with: cache-key: ${{ needs.cache.outputs.cache-key }} - python-version: ${{ steps.python.outputs.python-version }} + python-version: ${{ env.DEFAULT_PYTHON }} venv-dir: ${{ env.VENV }} precommit-home: ${{ env.PRE_COMMIT_HOME }} - name: Run complexity report (click to view details) From 7551d55c08df552c25faccc7772b4c2f12867a2e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 12 Jun 2025 15:05:36 +0200 Subject: [PATCH 25/71] Let set-switch functions return a bool, improve handling of locked state --- plugwise/__init__.py | 4 ++-- plugwise/legacy/smile.py | 22 +++++++++++----------- plugwise/smile.py | 20 ++++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 35250323d..65f6c51dd 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -398,10 +398,10 @@ async def set_temperature_offset(self, dev_id: str, offset: float) -> None: async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str - ) -> None: + ) -> bool: """Set the given State of the relevant Switch.""" try: - await self._smile_api.set_switch_state(appl_id, members, model, state) + return await self._smile_api.set_switch_state(appl_id, members, model, state) except ConnectionFailedError as exc: raise ConnectionFailedError( f"Failed to set switch state: {str(exc)}" diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index eedd360f7..6e9fdcdfb 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -234,7 +234,7 @@ async def set_schedule_state( async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str - ) -> None: + ) -> bool: """Set the given state of the relevant switch. For individual switches, sets the state directly. @@ -269,7 +269,7 @@ async def set_switch_state( "" ) await self.call_request(APPLIANCES, method="post", data=data) - return + return True # Handle group of switches data = f"<{switch.func_type}>{state}" @@ -280,26 +280,26 @@ async def set_switch_state( # Handle individual relay switches uri = f"{APPLIANCES};id={appl_id}/relay" - if model == "relay": - locator = ( - f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock' - ) + if model == "relay" and self.gw_entities[appl_id]["switches"]["lock"]: # Don't bother switching a relay when the corresponding lock-state is true - if self._appliances.find(locator).text == "true": - raise PlugwiseError("Plugwise: the locked Relay was not switched.") + return False await self.call_request(uri, method="put", data=data) + return True async def _set_groupswitch_member_state( self, data: str, members: list[str], state: str, switch: Munch - ) -> None: + ) -> bool: """Helper-function for set_switch_state(). Set the given State of the relevant Switch (relay) within a group of members. """ for member in members: - uri = f"{APPLIANCES};id={member}/relay" - await self.call_request(uri, method="put", data=data) + if not self.gw_entities[member]["switches"]["lock"]: + uri = f"{APPLIANCES};id={member}/relay" + await self.call_request(uri, method="put", data=data) + + return True async def set_temperature(self, _: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" diff --git a/plugwise/smile.py b/plugwise/smile.py index f3b259ff8..af0ffad38 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -377,7 +377,7 @@ def determine_contexts( async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str - ) -> None: + ) -> bool: """Set the given State of the relevant Switch.""" switch = Munch() switch.actuator = "actuator_functionalities" @@ -418,19 +418,16 @@ async def set_switch_state( f"" ) uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}" - if model == "relay": - locator = ( - f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}/lock' - ) - # Don't bother switching a relay when the corresponding lock-state is true - if self._domain_objects.find(locator).text == "true": - raise PlugwiseError("Plugwise: the locked Relay was not switched.") + if model == "relay" and self.gw_entities[appl_id]["switches"]["lock"]: + # Don't switch a relay when its corresponding lock-state is true + return False await self.call_request(uri, method="put", data=data) + return True async def _set_groupswitch_member_state( self, members: list[str], state: str, switch: Munch - ) -> None: + ) -> bool: """Helper-function for set_switch_state(). Set the given State of the relevant Switch within a group of members. @@ -444,7 +441,10 @@ async def _set_groupswitch_member_state( f"<{switch.func}>{state}" f"" ) - await self.call_request(uri, method="put", data=data) + if not self.gw_entities[member]["switches"].get("lock"): + await self.call_request(uri, method="put", data=data) + + return True async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" From f89fe4ede37d1394df4c6a56546afdd284afa124 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Thu, 12 Jun 2025 15:25:51 +0200 Subject: [PATCH 26/71] Adapt tinker_switch() test function --- tests/test_init.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 0369f6503..3f2980263 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -688,8 +688,9 @@ async def tinker_switch( for new_state in ["false", "true", "false"]: _LOGGER.info("- Switching %s", new_state) try: - await smile.set_switch_state(dev_id, members, model, new_state) - tinker_switch_passed = True + tinker_switch_passed = await smile.set_switch_state( + dev_id, members, model, new_state + ) _LOGGER.info(" + tinker_switch worked as intended") except pw_exceptions.PlugwiseError: _LOGGER.info(" + locked, not switched as expected") From 46e05d64547e6ce3a8ba2b8122daff823f6a635e Mon Sep 17 00:00:00 2001 From: autoruff Date: Thu, 12 Jun 2025 14:02:36 +0000 Subject: [PATCH 27/71] fixup: relay-sw-improve Python code fixed using ruff --- plugwise/__init__.py | 4 +++- plugwise/legacy/smile.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 65f6c51dd..8cce15321 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -401,7 +401,9 @@ async def set_switch_state( ) -> bool: """Set the given State of the relevant Switch.""" try: - return await self._smile_api.set_switch_state(appl_id, members, model, state) + return await self._smile_api.set_switch_state( + appl_id, members, model, state + ) except ConnectionFailedError as exc: raise ConnectionFailedError( f"Failed to set switch state: {str(exc)}" diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index 6e9fdcdfb..e5257d55a 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -298,7 +298,7 @@ async def _set_groupswitch_member_state( if not self.gw_entities[member]["switches"]["lock"]: uri = f"{APPLIANCES};id={member}/relay" await self.call_request(uri, method="put", data=data) - + return True async def set_temperature(self, _: str, items: dict[str, float]) -> None: From 6a1634fb0bea084e83f08202d451d0d3236f2a07 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 08:07:40 +0200 Subject: [PATCH 28/71] Return fixes --- plugwise/constants.py | 1 + plugwise/legacy/smile.py | 8 ++++++-- plugwise/smile.py | 8 ++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/plugwise/constants.py b/plugwise/constants.py index f12c8bbd9..5d306e9a3 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -23,6 +23,7 @@ PRESET_AWAY: Final = "away" PRESSURE_BAR: Final = "bar" SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm" +STATE_ON: Final = "on" TEMP_CELSIUS: Final = "°C" TEMP_KELVIN: Final = "°K" TIME_MILLISECONDS: Final = "ms" diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index e5257d55a..ca1792984 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -18,6 +18,7 @@ OFF, REQUIRE_APPLIANCES, RULES, + STATE_ON, GwEntityData, ThermoLoc, ) @@ -241,6 +242,7 @@ async def set_switch_state( For group switches, sets the state for each member in the group separately. For switch-locks, sets the lock state using a different data format. """ + req_state = state == STATE_ON switch = Munch() switch.actuator = "actuator_functionalities" switch.func_type = "relay_functionality" @@ -285,7 +287,7 @@ async def set_switch_state( return False await self.call_request(uri, method="put", data=data) - return True + return req_state async def _set_groupswitch_member_state( self, data: str, members: list[str], state: str, switch: Munch @@ -294,12 +296,14 @@ async def _set_groupswitch_member_state( Set the given State of the relevant Switch (relay) within a group of members. """ + switched = 0 for member in members: if not self.gw_entities[member]["switches"]["lock"]: uri = f"{APPLIANCES};id={member}/relay" await self.call_request(uri, method="put", data=data) + switched += 1 - return True + return switched > 0 async def set_temperature(self, _: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" diff --git a/plugwise/smile.py b/plugwise/smile.py index af0ffad38..9fb8547cd 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -22,6 +22,7 @@ NOTIFICATIONS, OFF, RULES, + STATE_ON, GwEntityData, ThermoLoc, ) @@ -379,6 +380,7 @@ async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: """Set the given State of the relevant Switch.""" + req_state = state == STATE_ON switch = Munch() switch.actuator = "actuator_functionalities" switch.device = "relay" @@ -423,7 +425,7 @@ async def set_switch_state( return False await self.call_request(uri, method="put", data=data) - return True + return req_state async def _set_groupswitch_member_state( self, members: list[str], state: str, switch: Munch @@ -432,6 +434,7 @@ async def _set_groupswitch_member_state( Set the given State of the relevant Switch within a group of members. """ + switched = 0 for member in members: locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}' switch_id = self._domain_objects.find(locator).attrib["id"] @@ -443,8 +446,9 @@ async def _set_groupswitch_member_state( ) if not self.gw_entities[member]["switches"].get("lock"): await self.call_request(uri, method="put", data=data) + switched += 1 - return True + return switched > 0 async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" From e3edfe96401a4fdb257de1f41304c8ae1f1a6d1e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 08:24:47 +0200 Subject: [PATCH 29/71] Improve test --- tests/test_init.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 3f2980263..986148e00 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -688,10 +688,15 @@ async def tinker_switch( for new_state in ["false", "true", "false"]: _LOGGER.info("- Switching %s", new_state) try: - tinker_switch_passed = await smile.set_switch_state( + result = await smile.set_switch_state( dev_id, members, model, new_state ) - _LOGGER.info(" + tinker_switch worked as intended") + if result == new_state: + tinker_switch_passed = True + _LOGGER.info(" + tinker_switch worked as intended") + else: + _LOGGER.info(" + tinker_switch failed unexpectedly") + return False except pw_exceptions.PlugwiseError: _LOGGER.info(" + locked, not switched as expected") return False From 93d47b303d2a5fdb38c0a7f838c914fdf4d2b2c9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 08:31:23 +0200 Subject: [PATCH 30/71] More return fixes --- plugwise/legacy/smile.py | 6 +++++- plugwise/smile.py | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index ca1792984..b229b8745 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -296,6 +296,7 @@ async def _set_groupswitch_member_state( Set the given State of the relevant Switch (relay) within a group of members. """ + req_state = state == STATE_ON switched = 0 for member in members: if not self.gw_entities[member]["switches"]["lock"]: @@ -303,7 +304,10 @@ async def _set_groupswitch_member_state( await self.call_request(uri, method="put", data=data) switched += 1 - return switched > 0 + if switched > 0: + return req_state + else: + return not req_state async def set_temperature(self, _: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" diff --git a/plugwise/smile.py b/plugwise/smile.py index 9fb8547cd..e0b29f0b8 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -434,6 +434,7 @@ async def _set_groupswitch_member_state( Set the given State of the relevant Switch within a group of members. """ + req_state = state == STATE_ON switched = 0 for member in members: locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}' @@ -448,7 +449,11 @@ async def _set_groupswitch_member_state( await self.call_request(uri, method="put", data=data) switched += 1 - return switched > 0 + if switched > 0: + return req_state + else: + return not req_state + async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" From 323c39ac648fc772a342351e182cb2526e646a1f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 16:59:41 +0200 Subject: [PATCH 31/71] Fix expected state value --- plugwise/legacy/smile.py | 5 ++--- plugwise/smile.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index b229b8745..a4d452a45 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -18,7 +18,6 @@ OFF, REQUIRE_APPLIANCES, RULES, - STATE_ON, GwEntityData, ThermoLoc, ) @@ -242,7 +241,7 @@ async def set_switch_state( For group switches, sets the state for each member in the group separately. For switch-locks, sets the lock state using a different data format. """ - req_state = state == STATE_ON + req_state = state == "true" switch = Munch() switch.actuator = "actuator_functionalities" switch.func_type = "relay_functionality" @@ -296,7 +295,7 @@ async def _set_groupswitch_member_state( Set the given State of the relevant Switch (relay) within a group of members. """ - req_state = state == STATE_ON + req_state = state == "true" switched = 0 for member in members: if not self.gw_entities[member]["switches"]["lock"]: diff --git a/plugwise/smile.py b/plugwise/smile.py index e0b29f0b8..292363a97 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -22,7 +22,6 @@ NOTIFICATIONS, OFF, RULES, - STATE_ON, GwEntityData, ThermoLoc, ) @@ -380,7 +379,7 @@ async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: """Set the given State of the relevant Switch.""" - req_state = state == STATE_ON + req_state = state == "true" switch = Munch() switch.actuator = "actuator_functionalities" switch.device = "relay" @@ -434,7 +433,7 @@ async def _set_groupswitch_member_state( Set the given State of the relevant Switch within a group of members. """ - req_state = state == STATE_ON + req_state = state == "true" switched = 0 for member in members: locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}' From 0b2d616313b8962c100b52a084383fdef54eebb9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 17:06:59 +0200 Subject: [PATCH 32/71] Fix testcode --- tests/test_init.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_init.py b/tests/test_init.py index 986148e00..d804a1b88 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -684,6 +684,7 @@ async def tinker_switch( """Turn a Switch on and off to test functionality.""" _LOGGER.info("Asserting modifying settings for switch devices:") _LOGGER.info("- Devices (%s):", dev_id) + convert = {"true": True, "false": False} tinker_switch_passed = False for new_state in ["false", "true", "false"]: _LOGGER.info("- Switching %s", new_state) @@ -691,7 +692,7 @@ async def tinker_switch( result = await smile.set_switch_state( dev_id, members, model, new_state ) - if result == new_state: + if result == convert[new_state]: tinker_switch_passed = True _LOGGER.info(" + tinker_switch worked as intended") else: From 59c03a202ab42e10123d570f42c22ca8e15afbec Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 17:11:39 +0200 Subject: [PATCH 33/71] Fix legacy set_switch for lock --- plugwise/legacy/smile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index a4d452a45..62ebaefbc 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -270,7 +270,7 @@ async def set_switch_state( "" ) await self.call_request(APPLIANCES, method="post", data=data) - return True + return req_state # Handle group of switches data = f"<{switch.func_type}>{state}" From 2eb36bab68e2105df1da0160df96de75d85eb2dc Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 17:12:37 +0200 Subject: [PATCH 34/71] Ruff fix --- tests/test_init.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index d804a1b88..3216cb3c1 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -689,9 +689,7 @@ async def tinker_switch( for new_state in ["false", "true", "false"]: _LOGGER.info("- Switching %s", new_state) try: - result = await smile.set_switch_state( - dev_id, members, model, new_state - ) + result = await smile.set_switch_state(dev_id, members, model, new_state) if result == convert[new_state]: tinker_switch_passed = True _LOGGER.info(" + tinker_switch worked as intended") From 45b500fb9f03c6bc2199ddec1ea07f2ac3b5b96f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 17:26:24 +0200 Subject: [PATCH 35/71] Add testcase for a blocked switchgroup-change --- .../adam/adam_multiple_devices_per_zone.json | 13 +++++ tests/test_adam.py | 8 +++ .../core.domain_objects.xml | 56 ++++++++++++++++++- 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/tests/data/adam/adam_multiple_devices_per_zone.json b/tests/data/adam/adam_multiple_devices_per_zone.json index 523f9cfcc..1c0f73634 100644 --- a/tests/data/adam/adam_multiple_devices_per_zone.json +++ b/tests/data/adam/adam_multiple_devices_per_zone.json @@ -567,6 +567,19 @@ "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A08" }, + "e8ef2a01ed3b4139a53bf749204fe6b4": { + "dev_class": "switching", + "members": [ + "02cf28bfec924855854c544690a609ef", + "4a810418d5394b3f82727340b91ba740" + ], + "model": "Switchgroup", + "name": "Test", + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, "fe799307f1624099878210aa0b9f1475": { "binary_sensors": { "plugwise_notification": true diff --git a/tests/test_adam.py b/tests/test_adam.py index 2378d7f41..1be49423c 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -304,6 +304,14 @@ async def test_connect_adam_multiple_devices_per_zone(self): smile, "675416a629f343c495449970e2ca37b5" ) assert not switch_change + # Test a blocked group-change, both relays are locked. + group_change = await self.tinker_switch( + smile, + "e8ef2a01ed3b4139a53bf749204fe6b4", + ["02cf28bfec924855854c544690a609ef", "4a810418d5394b3f82727340b91ba740"], + ) + assert not group_change + await smile.close_connection() await self.disconnect(server, client) diff --git a/userdata/adam_multiple_devices_per_zone/core.domain_objects.xml b/userdata/adam_multiple_devices_per_zone/core.domain_objects.xml index e38e3a9b1..9a91994ab 100644 --- a/userdata/adam_multiple_devices_per_zone/core.domain_objects.xml +++ b/userdata/adam_multiple_devices_per_zone/core.domain_objects.xml @@ -1425,7 +1425,9 @@ 2020-03-20T17:44:58.716+01:00 - + + + 2020-03-20T17:30:00+01:00 @@ -3146,7 +3148,9 @@ 2020-03-20T17:39:34.219+01:00 - + + + 2020-03-20T17:28:23.547+01:00 @@ -3517,6 +3521,54 @@ + + Test + + switching + 2021-12-23T08:25:07.571+01:00 + 2023-12-22T16:29:14.088+01:00 + + + + + + + + relay + + + + + + + electricity_produced + W + 2023-12-22T16:29:13.997+01:00 + 2023-08-16T23:58:55.515+02:00 + + + 0.00 + + + + electricity_consumed + W + 2023-12-22T16:29:13.997+01:00 + 2023-08-16T23:58:55.515+02:00 + + + 14.81 + + + + + + + false + single + + + From 68ed57ceab80557e75ed9f30d02fe42894390b56 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 17:33:07 +0200 Subject: [PATCH 36/71] Update related entity_items --- tests/test_adam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_adam.py b/tests/test_adam.py index 1be49423c..d1521e5eb 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -288,7 +288,7 @@ async def test_connect_adam_multiple_devices_per_zone(self): assert smile._last_active["82fa13f017d240daa0d0ea1775420f24"] == CV_JESSIE assert smile._last_active["08963fec7c53423ca5680aa4cb502c63"] == BADKAMER_SCHEMA assert smile._last_active["446ac08dd04d4eff8ac57489757b7314"] == BADKAMER_SCHEMA - assert self.entity_items == 370 + assert self.entity_items == 375 assert "af82e4ccf9c548528166d38e560662a4" in self.notifications From e0f35ab9f9daf3c3feb487b2439245e134172785 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 17:36:55 +0200 Subject: [PATCH 37/71] Save updates --- .../adam_multiple_devices_per_zone/data.json | 13 +++++++++++++ .../m_adam_multiple_devices_per_zone/data.json | 13 +++++++++++++ plugwise/smile.py | 1 - .../adam/adam_multiple_devices_per_zone.json | 16 ++++++++-------- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/fixtures/adam_multiple_devices_per_zone/data.json b/fixtures/adam_multiple_devices_per_zone/data.json index d3e13a175..8937cd465 100644 --- a/fixtures/adam_multiple_devices_per_zone/data.json +++ b/fixtures/adam_multiple_devices_per_zone/data.json @@ -540,6 +540,19 @@ "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A11" }, + "e8ef2a01ed3b4139a53bf749204fe6b4": { + "dev_class": "switching", + "members": [ + "02cf28bfec924855854c544690a609ef", + "4a810418d5394b3f82727340b91ba740" + ], + "model": "Switchgroup", + "name": "Test", + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, "f1fee6043d3642a9b0a65297455f008e": { "available": true, "binary_sensors": { diff --git a/fixtures/m_adam_multiple_devices_per_zone/data.json b/fixtures/m_adam_multiple_devices_per_zone/data.json index 7c38b1b21..06459a117 100644 --- a/fixtures/m_adam_multiple_devices_per_zone/data.json +++ b/fixtures/m_adam_multiple_devices_per_zone/data.json @@ -531,6 +531,19 @@ "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A11" }, + "e8ef2a01ed3b4139a53bf749204fe6b4": { + "dev_class": "switching", + "members": [ + "02cf28bfec924855854c544690a609ef", + "4a810418d5394b3f82727340b91ba740" + ], + "model": "Switchgroup", + "name": "Test", + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, "f1fee6043d3642a9b0a65297455f008e": { "available": true, "binary_sensors": { diff --git a/plugwise/smile.py b/plugwise/smile.py index 292363a97..e2b20f9e8 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -453,7 +453,6 @@ async def _set_groupswitch_member_state( else: return not req_state - async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" setpoint: float | None = None diff --git a/tests/data/adam/adam_multiple_devices_per_zone.json b/tests/data/adam/adam_multiple_devices_per_zone.json index 1c0f73634..2780afe76 100644 --- a/tests/data/adam/adam_multiple_devices_per_zone.json +++ b/tests/data/adam/adam_multiple_devices_per_zone.json @@ -568,17 +568,17 @@ "zigbee_mac_address": "ABCD012345670A08" }, "e8ef2a01ed3b4139a53bf749204fe6b4": { - "dev_class": "switching", - "members": [ + "dev_class": "switching", + "members": [ "02cf28bfec924855854c544690a609ef", "4a810418d5394b3f82727340b91ba740" - ], - "model": "Switchgroup", - "name": "Test", - "switches": { + ], + "model": "Switchgroup", + "name": "Test", + "switches": { "relay": true - }, - "vendor": "Plugwise" + }, + "vendor": "Plugwise" }, "fe799307f1624099878210aa0b9f1475": { "binary_sensors": { From 75fae4f4920a1f6f9822a93c04d29a0a9c66a938 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 17:39:29 +0200 Subject: [PATCH 38/71] Add pragma-no-cover for legacy --- plugwise/legacy/smile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index 62ebaefbc..df932cc12 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -306,7 +306,7 @@ async def _set_groupswitch_member_state( if switched > 0: return req_state else: - return not req_state + return not req_state # pragma: no cover async def set_temperature(self, _: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" @@ -317,7 +317,7 @@ async def set_temperature(self, _: str, items: dict[str, float]) -> None: if setpoint is None: raise PlugwiseError( "Plugwise: failed setting temperature: no valid input provided" - ) # pragma: no cover" + ) # pragma: no cover temperature = str(setpoint) data = ( From 9f48e392e071e17badac7058f70743b702c7b7d9 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 17:40:47 +0200 Subject: [PATCH 39/71] Test: clear no longer expected exception --- tests/test_init.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_init.py b/tests/test_init.py index 3216cb3c1..69a9b9d8f 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -696,9 +696,6 @@ async def tinker_switch( else: _LOGGER.info(" + tinker_switch failed unexpectedly") return False - except pw_exceptions.PlugwiseError: - _LOGGER.info(" + locked, not switched as expected") - return False except ( pw_exceptions.ConnectionFailedError ): # leave for-loop at connect-error From a9abbe3dd9ad6cb140a869cf55d890ef88ae184c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 18:11:43 +0200 Subject: [PATCH 40/71] Bump to v1.7.6a0 test-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a2dd35387..c8d1067fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise" -version = "1.7.5" +version = "1.7.6a0" license = "MIT" description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3." readme = "README.md" From 93b9b2d52abdbf4d2c253f0e8847869a8e7cd127 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 19:29:01 +0200 Subject: [PATCH 41/71] Correct switch state input --- plugwise/legacy/smile.py | 7 ++++--- plugwise/smile.py | 7 ++++--- tests/test_init.py | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index df932cc12..0dae3ab72 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -18,6 +18,7 @@ OFF, REQUIRE_APPLIANCES, RULES, + STATE_ON, GwEntityData, ThermoLoc, ) @@ -241,7 +242,7 @@ async def set_switch_state( For group switches, sets the state for each member in the group separately. For switch-locks, sets the lock state using a different data format. """ - req_state = state == "true" + req_state = state == STATE_ON switch = Munch() switch.actuator = "actuator_functionalities" switch.func_type = "relay_functionality" @@ -251,7 +252,7 @@ async def set_switch_state( # Handle switch-lock if model == "lock": - state = "false" if state == "off" else "true" + state = "true" if state == STATE_ON else "false" appliance = self._appliances.find(f'appliance[@id="{appl_id}"]') appl_name = appliance.find("name").text appl_type = appliance.find("type").text @@ -295,7 +296,7 @@ async def _set_groupswitch_member_state( Set the given State of the relevant Switch (relay) within a group of members. """ - req_state = state == "true" + req_state = state == STATE_ON switched = 0 for member in members: if not self.gw_entities[member]["switches"]["lock"]: diff --git a/plugwise/smile.py b/plugwise/smile.py index e2b20f9e8..cff95913c 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -22,6 +22,7 @@ NOTIFICATIONS, OFF, RULES, + STATE_ON, GwEntityData, ThermoLoc, ) @@ -379,7 +380,7 @@ async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: """Set the given State of the relevant Switch.""" - req_state = state == "true" + req_state = state == STATE_ON switch = Munch() switch.actuator = "actuator_functionalities" switch.device = "relay" @@ -397,7 +398,7 @@ async def set_switch_state( if model == "lock": switch.func = "lock" - state = "false" if state == "off" else "true" + state = "true" if state == STATE_ON else "false" if members is not None: return await self._set_groupswitch_member_state(members, state, switch) @@ -433,7 +434,7 @@ async def _set_groupswitch_member_state( Set the given State of the relevant Switch within a group of members. """ - req_state = state == "true" + req_state = state == STATE_ON switched = 0 for member in members: locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}' diff --git a/tests/test_init.py b/tests/test_init.py index 69a9b9d8f..b6bf1bd6d 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -684,9 +684,9 @@ async def tinker_switch( """Turn a Switch on and off to test functionality.""" _LOGGER.info("Asserting modifying settings for switch devices:") _LOGGER.info("- Devices (%s):", dev_id) - convert = {"true": True, "false": False} + convert = {"on": True, "off": False} tinker_switch_passed = False - for new_state in ["false", "true", "false"]: + for new_state in ["off", "on", "off"]: _LOGGER.info("- Switching %s", new_state) try: result = await smile.set_switch_state(dev_id, members, model, new_state) From 8a9085c7c46e34a036b6f2e18f2e4cfc5c6c77d2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 19:30:08 +0200 Subject: [PATCH 42/71] Bump to a1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c8d1067fe..c42ea7b5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise" -version = "1.7.6a0" +version = "1.7.6a1" license = "MIT" description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3." readme = "README.md" From 31bc6a4b831df2cd87d225e6cc1ceb74667d0048 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 13 Jun 2025 20:14:19 +0200 Subject: [PATCH 43/71] Improve var-name, correct returns --- plugwise/legacy/smile.py | 16 ++++++++-------- plugwise/smile.py | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index 0dae3ab72..915bc2cff 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -242,7 +242,7 @@ async def set_switch_state( For group switches, sets the state for each member in the group separately. For switch-locks, sets the lock state using a different data format. """ - req_state = state == STATE_ON + requested_state = state == STATE_ON switch = Munch() switch.actuator = "actuator_functionalities" switch.func_type = "relay_functionality" @@ -271,7 +271,7 @@ async def set_switch_state( "" ) await self.call_request(APPLIANCES, method="post", data=data) - return req_state + return requested_state # Handle group of switches data = f"<{switch.func_type}>{state}" @@ -284,10 +284,10 @@ async def set_switch_state( uri = f"{APPLIANCES};id={appl_id}/relay" if model == "relay" and self.gw_entities[appl_id]["switches"]["lock"]: # Don't bother switching a relay when the corresponding lock-state is true - return False + return not requested_state await self.call_request(uri, method="put", data=data) - return req_state + return requested_state async def _set_groupswitch_member_state( self, data: str, members: list[str], state: str, switch: Munch @@ -296,7 +296,7 @@ async def _set_groupswitch_member_state( Set the given State of the relevant Switch (relay) within a group of members. """ - req_state = state == STATE_ON + requested_state = state == STATE_ON switched = 0 for member in members: if not self.gw_entities[member]["switches"]["lock"]: @@ -305,9 +305,9 @@ async def _set_groupswitch_member_state( switched += 1 if switched > 0: - return req_state - else: - return not req_state # pragma: no cover + return requested_state + + return not requested_state # pragma: no cover async def set_temperature(self, _: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" diff --git a/plugwise/smile.py b/plugwise/smile.py index cff95913c..5b0c2dcae 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -380,7 +380,7 @@ async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: """Set the given State of the relevant Switch.""" - req_state = state == STATE_ON + requested_state = state == STATE_ON switch = Munch() switch.actuator = "actuator_functionalities" switch.device = "relay" @@ -422,10 +422,10 @@ async def set_switch_state( uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}" if model == "relay" and self.gw_entities[appl_id]["switches"]["lock"]: # Don't switch a relay when its corresponding lock-state is true - return False + return not requested_state await self.call_request(uri, method="put", data=data) - return req_state + return requested_state async def _set_groupswitch_member_state( self, members: list[str], state: str, switch: Munch @@ -434,7 +434,7 @@ async def _set_groupswitch_member_state( Set the given State of the relevant Switch within a group of members. """ - req_state = state == STATE_ON + requested_state = state == STATE_ON switched = 0 for member in members: locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}' @@ -450,9 +450,9 @@ async def _set_groupswitch_member_state( switched += 1 if switched > 0: - return req_state - else: - return not req_state + return requested_state + + return not requested_state async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" From bae729709b32a38c0ad71bae1453bedeb18dca72 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 14 Jun 2025 08:01:04 +0200 Subject: [PATCH 44/71] Bump to a2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c42ea7b5a..dbf61458c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise" -version = "1.7.6a1" +version = "1.7.6a2" license = "MIT" description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3." readme = "README.md" From a25ee26e1c41d98ad4e1dbd660908546a69c5628 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 14:35:38 +0200 Subject: [PATCH 45/71] Implement improvements as suggested --- plugwise/legacy/smile.py | 12 +++++++----- plugwise/smile.py | 30 +++++++++++++++--------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index 915bc2cff..c5b63e274 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -242,6 +242,7 @@ async def set_switch_state( For group switches, sets the state for each member in the group separately. For switch-locks, sets the lock state using a different data format. """ + current_state = self.gw_entities[appl_id]["switches"]["relay"] requested_state = state == STATE_ON switch = Munch() switch.actuator = "actuator_functionalities" @@ -277,25 +278,26 @@ async def set_switch_state( data = f"<{switch.func_type}>{state}" if members is not None: return await self._set_groupswitch_member_state( - data, members, state, switch + appl_id, data, members, state, switch ) # Handle individual relay switches uri = f"{APPLIANCES};id={appl_id}/relay" - if model == "relay" and self.gw_entities[appl_id]["switches"]["lock"]: + if model == "relay" and self.gw_entities[appl_id]["switches"].get("lock"): # Don't bother switching a relay when the corresponding lock-state is true - return not requested_state + return current_state await self.call_request(uri, method="put", data=data) return requested_state async def _set_groupswitch_member_state( - self, data: str, members: list[str], state: str, switch: Munch + self, appl_id: str, data: str, members: list[str], state: str, switch: Munch ) -> bool: """Helper-function for set_switch_state(). Set the given State of the relevant Switch (relay) within a group of members. """ + current_state = self.gw_entities[appl_id]["switches"]["relay"] requested_state = state == STATE_ON switched = 0 for member in members: @@ -307,7 +309,7 @@ async def _set_groupswitch_member_state( if switched > 0: return requested_state - return not requested_state # pragma: no cover + return current_state # pragma: no cover async def set_temperature(self, _: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" diff --git a/plugwise/smile.py b/plugwise/smile.py index 5b0c2dcae..6c3416a60 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -380,6 +380,7 @@ async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: """Set the given State of the relevant Switch.""" + current_state = self.gw_entities[appl_id]["switches"]["relay"] requested_state = state == STATE_ON switch = Munch() switch.actuator = "actuator_functionalities" @@ -400,8 +401,16 @@ async def set_switch_state( switch.func = "lock" state = "true" if state == STATE_ON else "false" + data = ( + f"<{switch.func_type}>" + f"<{switch.func}>{state}" + f"" + ) + if members is not None: - return await self._set_groupswitch_member_state(members, state, switch) + return await self._set_groupswitch_member_state( + appl_id, data, members, state, switch + ) locator = f'appliance[@id="{appl_id}"]/{switch.actuator}/{switch.func_type}' found = self._domain_objects.findall(locator) @@ -414,37 +423,28 @@ async def set_switch_state( else: # actuators with a single item like relay_functionality switch_id = item.attrib["id"] - data = ( - f"<{switch.func_type}>" - f"<{switch.func}>{state}" - f"" - ) uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}" - if model == "relay" and self.gw_entities[appl_id]["switches"]["lock"]: + if model == "relay" and self.gw_entities[appl_id]["switches"].get("lock"): # Don't switch a relay when its corresponding lock-state is true - return not requested_state + return current_state await self.call_request(uri, method="put", data=data) return requested_state async def _set_groupswitch_member_state( - self, members: list[str], state: str, switch: Munch + self, appl_id: str, data: str, members: list[str], state: str, switch: Munch ) -> bool: """Helper-function for set_switch_state(). Set the given State of the relevant Switch within a group of members. """ + current_state = self.gw_entities[appl_id]["switches"]["relay"] requested_state = state == STATE_ON switched = 0 for member in members: locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}' switch_id = self._domain_objects.find(locator).attrib["id"] uri = f"{APPLIANCES};id={member}/{switch.device};id={switch_id}" - data = ( - f"<{switch.func_type}>" - f"<{switch.func}>{state}" - f"" - ) if not self.gw_entities[member]["switches"].get("lock"): await self.call_request(uri, method="put", data=data) switched += 1 @@ -452,7 +452,7 @@ async def _set_groupswitch_member_state( if switched > 0: return requested_state - return not requested_state + return current_state async def set_temperature(self, loc_id: str, items: dict[str, float]) -> None: """Set the given Temperature on the relevant Thermostat.""" From 889006521a17765cf897b1d02bec6a8522589547 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 14:39:12 +0200 Subject: [PATCH 46/71] Handle no lock-attribute present --- plugwise/smile.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugwise/smile.py b/plugwise/smile.py index 6c3416a60..e916e5d5b 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -380,7 +380,7 @@ async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: """Set the given State of the relevant Switch.""" - current_state = self.gw_entities[appl_id]["switches"]["relay"] + current_state = self.gw_entities[appl_id]["switches"][model] requested_state = state == STATE_ON switch = Munch() switch.actuator = "actuator_functionalities" @@ -425,7 +425,8 @@ async def set_switch_state( uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}" if model == "relay" and self.gw_entities[appl_id]["switches"].get("lock"): - # Don't switch a relay when its corresponding lock-state is true + # Don't switch a relay when its corresponding lock-state is true or no + # lock is present. That means the relay can't be controlled by the user. return current_state await self.call_request(uri, method="put", data=data) From 0032a9059c987109b3cc3a61880fff2dd90b0ab2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 14:50:41 +0200 Subject: [PATCH 47/71] Correct testcase --- tests/test_adam.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_adam.py b/tests/test_adam.py index d1521e5eb..4383f0ab6 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -106,10 +106,11 @@ async def test_connect_adam_plus_anna_new(self): smile, "056ee145a816487eaa69243c3280f8bf", model="dhw_cm_switch" ) assert switch_change + # Test relay without lock-attribute switch_change = await self.tinker_switch( - smile, "854f8a9b0e7e425db97f1f110e1ce4b3", model="lock" + smile, "854f8a9b0e7e425db97f1f110e1ce4b3", ) - assert switch_change + assert not switch_change switch_change = await self.tinker_switch( smile, "2568cc4b9c1e401495d4741a5f89bee1" ) From ca3fd2c7a8befd8dedb07b9df0ac4525e8312036 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 14:56:28 +0200 Subject: [PATCH 48/71] Debug --- plugwise/smile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugwise/smile.py b/plugwise/smile.py index e916e5d5b..63e8cfe75 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -16,6 +16,7 @@ DOMAIN_OBJECTS, GATEWAY_REBOOT, LOCATIONS, + LOGGER, MAX_SETPOINT, MIN_SETPOINT, NONE, @@ -424,7 +425,9 @@ async def set_switch_state( switch_id = item.attrib["id"] uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}" - if model == "relay" and self.gw_entities[appl_id]["switches"].get("lock"): + lock_blocked = self.gw_entities[appl_id]["switches"].get("lock") + LOGGER.debug("HOI lock_blocked=%s", lock_blocked) + if model == "relay" and lock_blocked: # Don't switch a relay when its corresponding lock-state is true or no # lock is present. That means the relay can't be controlled by the user. return current_state From 806752adf4e8c893ca9a71e01bd06a650f377036 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 15:03:56 +0200 Subject: [PATCH 49/71] Try --- plugwise/smile.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugwise/smile.py b/plugwise/smile.py index 63e8cfe75..edcaa9452 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -425,12 +425,13 @@ async def set_switch_state( switch_id = item.attrib["id"] uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}" - lock_blocked = self.gw_entities[appl_id]["switches"].get("lock") - LOGGER.debug("HOI lock_blocked=%s", lock_blocked) - if model == "relay" and lock_blocked: + if model == "relay": + lock_blocked = self.gw_entities[appl_id]["switches"].get("lock") + LOGGER.debug("HOI lock_blocked=%s", lock_blocked) + if lock_blocked or lock_blocked is None: # Don't switch a relay when its corresponding lock-state is true or no # lock is present. That means the relay can't be controlled by the user. - return current_state + return current_state await self.call_request(uri, method="put", data=data) return requested_state From e4adde60e78e6a680bcbc691335b80da4bd58fff Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 15:05:36 +0200 Subject: [PATCH 50/71] Ruff fixes --- plugwise/smile.py | 4 ++-- tests/test_adam.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugwise/smile.py b/plugwise/smile.py index edcaa9452..e441d16c8 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -429,8 +429,8 @@ async def set_switch_state( lock_blocked = self.gw_entities[appl_id]["switches"].get("lock") LOGGER.debug("HOI lock_blocked=%s", lock_blocked) if lock_blocked or lock_blocked is None: - # Don't switch a relay when its corresponding lock-state is true or no - # lock is present. That means the relay can't be controlled by the user. + # Don't switch a relay when its corresponding lock-state is true or no + # lock is present. That means the relay can't be controlled by the user. return current_state await self.call_request(uri, method="put", data=data) diff --git a/tests/test_adam.py b/tests/test_adam.py index 4383f0ab6..8380b656f 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -108,7 +108,8 @@ async def test_connect_adam_plus_anna_new(self): assert switch_change # Test relay without lock-attribute switch_change = await self.tinker_switch( - smile, "854f8a9b0e7e425db97f1f110e1ce4b3", + smile, + "854f8a9b0e7e425db97f1f110e1ce4b3", ) assert not switch_change switch_change = await self.tinker_switch( From 4cf708ec35987073fb9bf1d094b6d4f95fd3d405 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 15:06:17 +0200 Subject: [PATCH 51/71] Clean up --- plugwise/smile.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugwise/smile.py b/plugwise/smile.py index e441d16c8..7849ecd06 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -16,7 +16,6 @@ DOMAIN_OBJECTS, GATEWAY_REBOOT, LOCATIONS, - LOGGER, MAX_SETPOINT, MIN_SETPOINT, NONE, @@ -427,7 +426,6 @@ async def set_switch_state( uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}" if model == "relay": lock_blocked = self.gw_entities[appl_id]["switches"].get("lock") - LOGGER.debug("HOI lock_blocked=%s", lock_blocked) if lock_blocked or lock_blocked is None: # Don't switch a relay when its corresponding lock-state is true or no # lock is present. That means the relay can't be controlled by the user. From e4c5825e99f42f54e3d47f6c8d505d21419e7e85 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 15:09:47 +0200 Subject: [PATCH 52/71] Add testcase covering switching of lock --- tests/test_adam.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_adam.py b/tests/test_adam.py index 8380b656f..5e2210f7a 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -116,6 +116,10 @@ async def test_connect_adam_plus_anna_new(self): smile, "2568cc4b9c1e401495d4741a5f89bee1" ) assert not switch_change + switch_change = await self.tinker_switch( + smile, "2568cc4b9c1e401495d4741a5f89bee1", model="lock", + ) + assert switch_change tinkered = await self.tinker_gateway_mode(smile) assert not tinkered From a7dc2118b1cad0740ff6285f80b616c100187e8c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 15:10:27 +0200 Subject: [PATCH 53/71] Ruff fix --- tests/test_adam.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_adam.py b/tests/test_adam.py index 5e2210f7a..53567c177 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -117,7 +117,9 @@ async def test_connect_adam_plus_anna_new(self): ) assert not switch_change switch_change = await self.tinker_switch( - smile, "2568cc4b9c1e401495d4741a5f89bee1", model="lock", + smile, + "2568cc4b9c1e401495d4741a5f89bee1", + model="lock", ) assert switch_change From 8ae7acf2b7e37b7bb281620a9bafd1a591b4fd24 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 15:12:23 +0200 Subject: [PATCH 54/71] For legacy lock is always present --- plugwise/legacy/smile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index c5b63e274..b7fa7a571 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -283,7 +283,7 @@ async def set_switch_state( # Handle individual relay switches uri = f"{APPLIANCES};id={appl_id}/relay" - if model == "relay" and self.gw_entities[appl_id]["switches"].get("lock"): + if model == "relay" and self.gw_entities[appl_id]["switches"]["lock"]: # Don't bother switching a relay when the corresponding lock-state is true return current_state From c6438ba7e346108632f3ffbcbe4fd26ab61c432c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 15:18:40 +0200 Subject: [PATCH 55/71] Mypy fix --- plugwise/smile.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugwise/smile.py b/plugwise/smile.py index 7849ecd06..d9814f455 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -7,7 +7,7 @@ from collections.abc import Awaitable, Callable import datetime as dt -from typing import Any +from typing import Any, cast from plugwise.constants import ( ADAM, @@ -24,6 +24,7 @@ RULES, STATE_ON, GwEntityData, + SwitchType, ThermoLoc, ) from plugwise.data import SmileData @@ -380,7 +381,8 @@ async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: """Set the given State of the relevant Switch.""" - current_state = self.gw_entities[appl_id]["switches"][model] + model_type = cast(SwitchType, model) + current_state = self.gw_entities[appl_id]["switches"][model_type] requested_state = state == STATE_ON switch = Munch() switch.actuator = "actuator_functionalities" From 9687d4e54a75058a93899caa79ac79d5537c3f22 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 15 Jun 2025 15:25:44 +0200 Subject: [PATCH 56/71] Doc-string updates --- plugwise/legacy/smile.py | 2 ++ plugwise/smile.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index b7fa7a571..14277589d 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -241,6 +241,7 @@ async def set_switch_state( For individual switches, sets the state directly. For group switches, sets the state for each member in the group separately. For switch-locks, sets the lock state using a different data format. + Return the requested state when succesful, the current state otherwise. """ current_state = self.gw_entities[appl_id]["switches"]["relay"] requested_state = state == STATE_ON @@ -296,6 +297,7 @@ async def _set_groupswitch_member_state( """Helper-function for set_switch_state(). Set the given State of the relevant Switch (relay) within a group of members. + Return the requested state when at least one requested change was succesful, the current state otherwise. """ current_state = self.gw_entities[appl_id]["switches"]["relay"] requested_state = state == STATE_ON diff --git a/plugwise/smile.py b/plugwise/smile.py index d9814f455..875e8e6d8 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -380,7 +380,10 @@ def determine_contexts( async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: - """Set the given State of the relevant Switch.""" + """Set the given State of the relevant Switch. + + Return the requested state when succesful, the current state otherwise. + """ model_type = cast(SwitchType, model) current_state = self.gw_entities[appl_id]["switches"][model_type] requested_state = state == STATE_ON @@ -442,6 +445,7 @@ async def _set_groupswitch_member_state( """Helper-function for set_switch_state(). Set the given State of the relevant Switch within a group of members. + Return the requested state when at least one requested change was succesful, the current state otherwise. """ current_state = self.gw_entities[appl_id]["switches"]["relay"] requested_state = state == STATE_ON From aae419148c6bef9e050b06fe4659e2b4c3536f4e Mon Sep 17 00:00:00 2001 From: autoruff Date: Sun, 15 Jun 2025 13:37:01 +0000 Subject: [PATCH 57/71] fixup: relay-sw-improve Python code fixed using ruff --- plugwise/smile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/smile.py b/plugwise/smile.py index 875e8e6d8..792a64719 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -381,7 +381,7 @@ async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: """Set the given State of the relevant Switch. - + Return the requested state when succesful, the current state otherwise. """ model_type = cast(SwitchType, model) From a10fdaee22e048bb83e6a1b296a0ee638adb3d40 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 15:34:00 +0200 Subject: [PATCH 58/71] Bump to a3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dbf61458c..c6b2297ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise" -version = "1.7.6a2" +version = "1.7.6a3" license = "MIT" description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3." readme = "README.md" From aa3803558b6210d8139e4a83b1178b738858a752 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 15:43:10 +0200 Subject: [PATCH 59/71] Improve docstrings --- plugwise/__init__.py | 8 +++++++- plugwise/legacy/smile.py | 4 ++-- plugwise/smile.py | 9 ++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 8cce15321..95add6d1e 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -399,7 +399,13 @@ async def set_temperature_offset(self, dev_id: str, offset: float) -> None: async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: - """Set the given State of the relevant Switch.""" + """Set the given State of the relevant Switch. + + Return the result: + - True when switched to state on, + - False when switched to state off, + - the unchanged state when the switch is for instance locked. + """ try: return await self._smile_api.set_switch_state( appl_id, members, model, state diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index 14277589d..d3b410d16 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -296,8 +296,8 @@ async def _set_groupswitch_member_state( ) -> bool: """Helper-function for set_switch_state(). - Set the given State of the relevant Switch (relay) within a group of members. - Return the requested state when at least one requested change was succesful, the current state otherwise. + Set the requested state of the relevant switch within a group of switches. + Return the current group-state when none of the switches has changed its state, the requested state otherwise. """ current_state = self.gw_entities[appl_id]["switches"]["relay"] requested_state = state == STATE_ON diff --git a/plugwise/smile.py b/plugwise/smile.py index 792a64719..b2e47a3ac 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -380,8 +380,11 @@ def determine_contexts( async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: - """Set the given State of the relevant Switch. + """Set the given state of the relevant Switch. + For individual switches, sets the state directly. + For group switches, sets the state for each member in the group separately. + For switch-locks, sets the lock state using a different data format. Return the requested state when succesful, the current state otherwise. """ model_type = cast(SwitchType, model) @@ -444,8 +447,8 @@ async def _set_groupswitch_member_state( ) -> bool: """Helper-function for set_switch_state(). - Set the given State of the relevant Switch within a group of members. - Return the requested state when at least one requested change was succesful, the current state otherwise. + Set the requested state of the relevant switch within a group of switches. + Return the current group-state when none of the switches has changed its state, the requested state otherwise. """ current_state = self.gw_entities[appl_id]["switches"]["relay"] requested_state = state == STATE_ON From c511870776fa4e65a8396683126d90e2fdacc620 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 16:04:42 +0200 Subject: [PATCH 60/71] Add input-check as suggested --- plugwise/__init__.py | 3 +++ plugwise/constants.py | 1 + 2 files changed, 4 insertions(+) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 95add6d1e..4aa0e6f85 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -406,6 +406,9 @@ async def set_switch_state( - False when switched to state off, - the unchanged state when the switch is for instance locked. """ + if state not in (STATE_ON, STATE_OFF): + raise PlugwiseError("Invalid state supplied to set_switch_state") + try: return await self._smile_api.set_switch_state( appl_id, members, model, state diff --git a/plugwise/constants.py b/plugwise/constants.py index 5d306e9a3..4e1becd56 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -23,6 +23,7 @@ PRESET_AWAY: Final = "away" PRESSURE_BAR: Final = "bar" SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm" +STATE_OFF: Final = "off" STATE_ON: Final = "on" TEMP_CELSIUS: Final = "°C" TEMP_KELVIN: Final = "°K" From b06106a3794827267a63ec51c68129fc818ec591 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 16:17:42 +0200 Subject: [PATCH 61/71] Move preset input-checking to __init__.py, use constants --- plugwise/__init__.py | 4 ++++ plugwise/legacy/smile.py | 10 +++------- plugwise/smile.py | 14 +++++--------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 4aa0e6f85..ff2e3347d 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -359,6 +359,10 @@ async def set_schedule_state( async def set_preset(self, loc_id: str, preset: str) -> None: """Set the given Preset on the relevant Thermostat.""" + if (presets := self._presets(loc_id)) is None: + raise PlugwiseError("Plugwise: no presets available.") # pragma: no cover + if preset not in list(presets): + raise PlugwiseError("Plugwise: invalid preset.") try: await self._smile_api.set_preset(loc_id, preset) except ConnectionFailedError as exc: diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index d3b410d16..e5dea9969 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -18,6 +18,7 @@ OFF, REQUIRE_APPLIANCES, RULES, + STATE_OFF, STATE_ON, GwEntityData, ThermoLoc, @@ -168,11 +169,6 @@ async def set_offset(self, dev_id: str, offset: float) -> None: async def set_preset(self, _: str, preset: str) -> None: """Set the given Preset on the relevant Thermostat - from DOMAIN_OBJECTS.""" - if (presets := self._presets()) is None: - raise PlugwiseError("Plugwise: no presets available.") # pragma: no cover - if preset not in list(presets): - raise PlugwiseError("Plugwise: invalid preset.") - locator = f'rule/directives/when/then[@icon="{preset}"].../.../...' rule_id = self._domain_objects.find(locator).attrib["id"] data = f"true" @@ -196,7 +192,7 @@ async def set_schedule_state( Determined from - DOMAIN_OBJECTS. Used in HA Core to set the hvac_mode: in practice switch between schedule on - off. """ - if state not in ("on", "off"): + if state not in (STATE_OFF, STATE_ON): raise PlugwiseError("Plugwise: invalid schedule state.") # Handle no schedule-name / Off-schedule provided @@ -215,7 +211,7 @@ async def set_schedule_state( ) # pragma: no cover new_state = "false" - if state == "on": + if state == STATE_ON: new_state = "true" locator = f'.//*[@id="{schedule_rule_id}"]/template' diff --git a/plugwise/smile.py b/plugwise/smile.py index b2e47a3ac..705c644da 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -22,6 +22,7 @@ NOTIFICATIONS, OFF, RULES, + STATE_OFF, STATE_ON, GwEntityData, SwitchType, @@ -201,11 +202,6 @@ async def set_offset(self, dev_id: str, offset: float) -> None: async def set_preset(self, loc_id: str, preset: str) -> None: """Set the given Preset on the relevant Thermostat - from LOCATIONS.""" - if (presets := self._presets(loc_id)) is None: - raise PlugwiseError("Plugwise: no presets available.") # pragma: no cover - if preset not in list(presets): - raise PlugwiseError("Plugwise: invalid preset.") - current_location = self._domain_objects.find(f'location[@id="{loc_id}"]') location_name = current_location.find("name").text location_type = current_location.find("type").text @@ -311,12 +307,12 @@ async def set_schedule_state( Used in HA Core to set the hvac_mode: in practice switch between schedule on - off. """ # Input checking - if new_state not in ("on", "off"): + if new_state not in (STATE_OFF, STATE_ON): raise PlugwiseError("Plugwise: invalid schedule state.") # Translate selection of Off-schedule-option to disabling the active schedule if name == OFF: - new_state = "off" + new_state = STATE_OFF # Handle no schedule-name / Off-schedule provided if name is None or name == OFF: @@ -369,10 +365,10 @@ def determine_contexts( subject = f'' subject = etree.fromstring(subject) - if state == "off": + if state == STATE_OFF: self._last_active[loc_id] = name contexts.remove(subject) - if state == "on": + if state == STATE_ON: contexts.append(subject) return str(etree.tostring(contexts, encoding="unicode").rstrip()) From 9e80cfb75a047636335a3f8b204ca1a879f31669 Mon Sep 17 00:00:00 2001 From: autoruff Date: Mon, 23 Jun 2025 14:19:31 +0000 Subject: [PATCH 62/71] fixup: relay-sw-improve Python code fixed using ruff --- plugwise/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index ff2e3347d..4aa80c3f0 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -404,7 +404,7 @@ async def set_switch_state( self, appl_id: str, members: list[str] | None, model: str, state: str ) -> bool: """Set the given State of the relevant Switch. - + Return the result: - True when switched to state on, - False when switched to state off, From e602444a316dbbf38a28b30d75cfc0c43e211d37 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 16:23:02 +0200 Subject: [PATCH 63/71] Add missing constant imports --- plugwise/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 4aa80c3f0..e8069633b 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -15,6 +15,8 @@ MODULES, NONE, SMILES, + STATE_OFF, + STATE_ON, STATUS, SYSTEM, GwEntityData, @@ -410,7 +412,7 @@ async def set_switch_state( - False when switched to state off, - the unchanged state when the switch is for instance locked. """ - if state not in (STATE_ON, STATE_OFF): + if state not in (STATE_OFF, STATE_ON): raise PlugwiseError("Invalid state supplied to set_switch_state") try: From dca583513f69c01c4ac6a91221d7c5846efdfbc3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 16:31:25 +0200 Subject: [PATCH 64/71] Revert move preset input-checking --- plugwise/__init__.py | 4 ---- plugwise/legacy/smile.py | 5 +++++ plugwise/smile.py | 5 +++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/plugwise/__init__.py b/plugwise/__init__.py index e8069633b..9c56faa6a 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -361,10 +361,6 @@ async def set_schedule_state( async def set_preset(self, loc_id: str, preset: str) -> None: """Set the given Preset on the relevant Thermostat.""" - if (presets := self._presets(loc_id)) is None: - raise PlugwiseError("Plugwise: no presets available.") # pragma: no cover - if preset not in list(presets): - raise PlugwiseError("Plugwise: invalid preset.") try: await self._smile_api.set_preset(loc_id, preset) except ConnectionFailedError as exc: diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py index e5dea9969..fc4f3a2c5 100644 --- a/plugwise/legacy/smile.py +++ b/plugwise/legacy/smile.py @@ -169,6 +169,11 @@ async def set_offset(self, dev_id: str, offset: float) -> None: async def set_preset(self, _: str, preset: str) -> None: """Set the given Preset on the relevant Thermostat - from DOMAIN_OBJECTS.""" + if (presets := self._presets()) is None: + raise PlugwiseError("Plugwise: no presets available.") # pragma: no cover + if preset not in list(presets): + raise PlugwiseError("Plugwise: invalid preset.") + locator = f'rule/directives/when/then[@icon="{preset}"].../.../...' rule_id = self._domain_objects.find(locator).attrib["id"] data = f"true" diff --git a/plugwise/smile.py b/plugwise/smile.py index 705c644da..848205467 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -202,6 +202,11 @@ async def set_offset(self, dev_id: str, offset: float) -> None: async def set_preset(self, loc_id: str, preset: str) -> None: """Set the given Preset on the relevant Thermostat - from LOCATIONS.""" + if (presets := self._presets(loc_id)) is None: + raise PlugwiseError("Plugwise: no presets available.") # pragma: no cover + if preset not in list(presets): + raise PlugwiseError("Plugwise: invalid preset.") + current_location = self._domain_objects.find(f'location[@id="{loc_id}"]') location_name = current_location.find("name").text location_type = current_location.find("type").text From 05d53003a72e0bf5f3de6af07b23435b8a8d789f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 18:08:13 +0200 Subject: [PATCH 65/71] Add tinker_switch_bad_input() test-function --- tests/test_init.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_init.py b/tests/test_init.py index b6bf1bd6d..c0c9143bf 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -708,6 +708,19 @@ async def tinker_switch( return tinker_switch_passed + @pytest.mark.asyncio + async def tinker_switch_bad_input( + self, smile, dev_id=None, members=None, model="relay", unhappy=False + ): + _LOGGER.info("Test entering bad input set_switch_state:") + _LOGGER.info("- Devices (%s):", dev_id) + new_state = "false" + try: + result = await smile.set_switch_state(dev_id, members, model, new_state) + except pw_exceptions.PlugwiseError: + _LOGGER.info(" + failed input-check as expected") + return True # test is pass! + @pytest.mark.asyncio async def tinker_thermostat_temp( self, smile, loc_id, block_cooling=False, fail_cooling=False, unhappy=False From 089e156afdbb4d0a8b023021309e6def7dbcb07d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 18:09:33 +0200 Subject: [PATCH 66/71] And implement --- tests/test_adam.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_adam.py b/tests/test_adam.py index 53567c177..b38bb6937 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -123,6 +123,11 @@ async def test_connect_adam_plus_anna_new(self): ) assert switch_change + assert self.tinker_switch_bad_input( + smile, + "854f8a9b0e7e425db97f1f110e1ce4b3", + ) + tinkered = await self.tinker_gateway_mode(smile) assert not tinkered From 2b0bf399c9fc098be30d8053699f020be0bd6f64 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 18:12:54 +0200 Subject: [PATCH 67/71] Fixes --- tests/test_adam.py | 4 ++-- tests/test_init.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_adam.py b/tests/test_adam.py index b38bb6937..96b015ce5 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -123,9 +123,9 @@ async def test_connect_adam_plus_anna_new(self): ) assert switch_change - assert self.tinker_switch_bad_input( + assert await self.tinker_switch_bad_input( smile, - "854f8a9b0e7e425db97f1f110e1ce4b3", + "854f8a9b0e7e425db97f1f110e1ce4b3", ) tinkered = await self.tinker_gateway_mode(smile) diff --git a/tests/test_init.py b/tests/test_init.py index c0c9143bf..c80468ec6 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -712,11 +712,12 @@ async def tinker_switch( async def tinker_switch_bad_input( self, smile, dev_id=None, members=None, model="relay", unhappy=False ): + """Enter a wrong state as input to toggle a Switch.""" _LOGGER.info("Test entering bad input set_switch_state:") _LOGGER.info("- Devices (%s):", dev_id) new_state = "false" try: - result = await smile.set_switch_state(dev_id, members, model, new_state) + await smile.set_switch_state(dev_id, members, model, new_state) except pw_exceptions.PlugwiseError: _LOGGER.info(" + failed input-check as expected") return True # test is pass! From 9246b5d61ff43e4e44efe72d9f1c58679dc0b40c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 19:54:50 +0200 Subject: [PATCH 68/71] Improve group-switch logic for Adam --- plugwise/smile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugwise/smile.py b/plugwise/smile.py index 848205467..0947401d4 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -458,7 +458,9 @@ async def _set_groupswitch_member_state( locator = f'appliance[@id="{member}"]/{switch.actuator}/{switch.func_type}' switch_id = self._domain_objects.find(locator).attrib["id"] uri = f"{APPLIANCES};id={member}/{switch.device};id={switch_id}" - if not self.gw_entities[member]["switches"].get("lock"): + lock_blocked = self.gw_entities[member]["switches"].get("lock") + # Assume Plugs under Plugwise control are not part of a group + if lock_blocked is not None and not lock_blocked: await self.call_request(uri, method="put", data=data) switched += 1 From 73cf3eaf1e6317e9b411058a7c4f85e40b3cb6c7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 20:04:45 +0200 Subject: [PATCH 69/71] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e02bd4d..614402d4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Changelog -## Ongoing +## v1.7.6 - Maintenance chores (mostly reworking Github CI Actions) backporting from efforts on Python Plugwise [USB: 264](https://github.com/plugwise/python-plugwise-usb/pull/264) after porting our progress using [USB: 263](https://github.com/plugwise/python-plugwise-usb/pull/263) +- Don't raise an error when a locked switch is being toggled, and other switch-related improvements via [755](https://github.com/plugwise/python-plugwise/pull/755) ## v1.7.5 From 8b1fda14690222711132a1dbea412b04be804a78 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 20:09:09 +0200 Subject: [PATCH 70/71] Bump to v1.7.6 release-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c6b2297ff..ede4e456a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise" -version = "1.7.6a3" +version = "1.7.6" license = "MIT" description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3." readme = "README.md" From da059006bf46238e69ac438731326844db7e0c29 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Mon, 23 Jun 2025 20:17:59 +0200 Subject: [PATCH 71/71] Use #issue --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 614402d4d..5d47c3971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ ## v1.7.6 -- Maintenance chores (mostly reworking Github CI Actions) backporting from efforts on Python Plugwise [USB: 264](https://github.com/plugwise/python-plugwise-usb/pull/264) after porting our progress using [USB: 263](https://github.com/plugwise/python-plugwise-usb/pull/263) -- Don't raise an error when a locked switch is being toggled, and other switch-related improvements via [755](https://github.com/plugwise/python-plugwise/pull/755) +- Maintenance chores (mostly reworking Github CI Actions) backporting from efforts on Python Plugwise [USB: #264](https://github.com/plugwise/python-plugwise-usb/pull/264) after porting our progress using [USB: #263](https://github.com/plugwise/python-plugwise-usb/pull/263) +- Don't raise an error when a locked switch is being toggled, and other switch-related improvements via [#755](https://github.com/plugwise/python-plugwise/pull/755) ## v1.7.5