diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index 984d90570..4683a4fb4 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -4,9 +4,10 @@
name: Latest commit
env:
- CACHE_VERSION: 12
+ CACHE_VERSION: 14
DEFAULT_PYTHON: "3.13"
PRE_COMMIT_HOME: ~/.cache/pre-commit
+ VENV: venv
on:
schedule:
@@ -16,10 +17,12 @@ on:
# pull_request:
jobs:
- # Prepare default python version environment
- prepare:
+ # Determine cache key once
+ cache:
runs-on: ubuntu-latest
- name: Prepare
+ name: Cache identify
+ outputs:
+ cache-key: ${{ steps.set-key.outputs.cache-key }}
steps:
- name: Check out committed code
uses: actions/checkout@v4
@@ -28,73 +31,48 @@ 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: 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"
+
+ # Prepare default python version environment
+ prepare:
+ runs-on: ubuntu-latest
+ needs: cache
+ name: Prepare
+ steps:
+ - name: Prepare code checkout and python/pre-commit setup
+ id: cache-reuse
+ uses: plugwise/gh-actions/prepare-python-and-code@v1
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: ${{ env.DEFAULT_PYTHON }}
+ 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
with:
persist-credentials: false
- - name: Set up Python ${{ env.DEFAULT_PYTHON }}
- id: python
- uses: actions/setup-python@v5
+ - name: Restore cached environment
+ id: cache-reuse
+ uses: plugwise/gh-actions/restore-venv@v1
with:
+ cache-key: ${{ needs.cache.outputs.cache-key }}
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
+ venv-dir: ${{ env.VENV }}
+ precommit-home: ${{ env.PRE_COMMIT_HOME }}
- name: Ruff (with fix)
run: |
. venv/bin/activate
@@ -115,44 +93,22 @@ jobs:
runs-on: ubuntu-latest
name: Check commit
needs:
+ - cache
+ - prepare
- ruff
- shellcheck
- dependencies_check
steps:
- name: Check out committed code
uses: actions/checkout@v4
- - name: Set up Python ${{ env.DEFAULT_PYTHON }}
- id: python
- uses: actions/setup-python@v5
+ - name: Restore cached environment
+ id: cache-reuse
+ uses: plugwise/gh-actions/restore-venv@v1
with:
+ cache-key: ${{ needs.cache.outputs.cache-key }}
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
- 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
+ venv-dir: ${{ env.VENV }}
+ precommit-home: ${{ env.PRE_COMMIT_HOME }}
- name: Verify commit
run: |
. venv/bin/activate
@@ -167,76 +123,27 @@ 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", "3.12"]
- 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", "3.12"]
-
+ 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
+ - name: Restore cached environment
+ id: cache-reuse
+ uses: plugwise/gh-actions/restore-venv@v1
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') }}
- - 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: ${{ env.DEFAULT_PYTHON }}
+ venv-dir: ${{ env.VENV }}
+ precommit-home: ${{ env.PRE_COMMIT_HOME }}
- name: Run all tests
run: |
. venv/bin/activate
@@ -252,32 +159,23 @@ 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
with:
persist-credentials: false
- - name: Set up Python ${{ env.DEFAULT_PYTHON }}
- id: python
- uses: actions/setup-python@v5
+ - name: Restore cached environment
+ id: cache-reuse
+ uses: plugwise/gh-actions/restore-venv@v1
with:
+ cache-key: ${{ needs.cache.outputs.cache-key }}
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
+ venv-dir: ${{ env.VENV }}
+ precommit-home: ${{ env.PRE_COMMIT_HOME }}
- name: Run mypy
run: |
. venv/bin/activate
@@ -307,30 +205,22 @@ 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
- - name: Set up Python ${{ env.DEFAULT_PYTHON }}
- id: python
- uses: actions/setup-python@v5
+ - name: Restore cached environment
+ id: cache-reuse
+ uses: plugwise/gh-actions/restore-venv@v1
with:
+ cache-key: ${{ needs.cache.outputs.cache-key }}
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 virtual environment from cache"
- exit 1
+ 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,38 +237,30 @@ 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
- - name: Set up Python ${{ env.DEFAULT_PYTHON }}
- id: python
- uses: actions/setup-python@v5
+ - name: Restore cached environment
+ id: cache-reuse
+ uses: plugwise/gh-actions/restore-venv@v1
with:
+ cache-key: ${{ needs.cache.outputs.cache-key }}
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 virtual environment from cache"
- exit 1
+ 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,30 +272,21 @@ 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
- - name: Set up Python ${{ env.DEFAULT_PYTHON }}
- id: python
- uses: actions/setup-python@v5
+ - name: Restore cached environment
+ id: cache-reuse
+ uses: plugwise/gh-actions/restore-venv@v1
with:
+ cache-key: ${{ needs.cache.outputs.cache-key }}
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 virtual environment from cache"
- exit 1
+ 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/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e804df1bb..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.7
+ rev: v0.12.0
hooks:
- id: ruff
name: "Ruff check"
@@ -24,20 +24,24 @@ 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"
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"
+ name: "Check Code Spelling"
+ args:
+ - --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: ^userdata/|^fixtures/
- repo: https://github.com/PyCQA/bandit
- rev: 1.8.3
+ rev: 1.8.5
hooks:
- id: bandit
name: "Bandit checking"
@@ -47,12 +51,19 @@ 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"
+ - 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.1
+ rev: v0.7.2
hooks:
# Run `python-typing-update` hook manually from time to time
# to update python typing syntax.
@@ -109,6 +120,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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 660a53a25..5d47c3971 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## 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
+
+- 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/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/__init__.py b/plugwise/__init__.py
index 35250323d..9c56faa6a 100644
--- a/plugwise/__init__.py
+++ b/plugwise/__init__.py
@@ -15,6 +15,8 @@
MODULES,
NONE,
SMILES,
+ STATE_OFF,
+ STATE_ON,
STATUS,
SYSTEM,
GwEntityData,
@@ -398,10 +400,21 @@ 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:
- """Set the given State of the relevant Switch."""
+ ) -> bool:
+ """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.
+ """
+ if state not in (STATE_OFF, STATE_ON):
+ raise PlugwiseError("Invalid state supplied to set_switch_state")
+
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/constants.py b/plugwise/constants.py
index f12c8bbd9..4e1becd56 100644
--- a/plugwise/constants.py
+++ b/plugwise/constants.py
@@ -23,6 +23,8 @@
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"
TIME_MILLISECONDS: Final = "ms"
diff --git a/plugwise/legacy/smile.py b/plugwise/legacy/smile.py
index eedd360f7..fc4f3a2c5 100644
--- a/plugwise/legacy/smile.py
+++ b/plugwise/legacy/smile.py
@@ -18,6 +18,8 @@
OFF,
REQUIRE_APPLIANCES,
RULES,
+ STATE_OFF,
+ STATE_ON,
GwEntityData,
ThermoLoc,
)
@@ -195,7 +197,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
@@ -214,7 +216,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'
@@ -234,13 +236,16 @@ 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.
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
switch = Munch()
switch.actuator = "actuator_functionalities"
switch.func_type = "relay_functionality"
@@ -250,7 +255,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
@@ -269,37 +274,45 @@ async def set_switch_state(
""
)
await self.call_request(APPLIANCES, method="post", data=data)
- return
+ return requested_state
# Handle group of switches
data = f"<{switch.func_type}>{state}{switch.func_type}>"
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":
- 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 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
- ) -> None:
+ 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.
+ 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
+ switched = 0
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)
+ switched += 1
+
+ if switched > 0:
+ return requested_state
+
+ 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."""
@@ -310,7 +323,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 = (
diff --git a/plugwise/smile.py b/plugwise/smile.py
index f3b259ff8..0947401d4 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,
@@ -22,7 +22,10 @@
NOTIFICATIONS,
OFF,
RULES,
+ STATE_OFF,
+ STATE_ON,
GwEntityData,
+ SwitchType,
ThermoLoc,
)
from plugwise.data import SmileData
@@ -309,12 +312,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:
@@ -367,18 +370,27 @@ 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())
async def set_switch_state(
self, appl_id: str, members: list[str] | None, model: str, state: str
- ) -> None:
- """Set the given State of the relevant Switch."""
+ ) -> bool:
+ """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)
+ current_state = self.gw_entities[appl_id]["switches"][model_type]
+ requested_state = state == STATE_ON
switch = Munch()
switch.actuator = "actuator_functionalities"
switch.device = "relay"
@@ -396,10 +408,18 @@ 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"
+
+ data = (
+ f"<{switch.func_type}>"
+ f"<{switch.func}>{state}{switch.func}>"
+ f"{switch.func_type}>"
+ )
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)
@@ -412,39 +432,42 @@ 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}{switch.func}>"
- f"{switch.func_type}>"
- )
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.")
+ lock_blocked = self.gw_entities[appl_id]["switches"].get("lock")
+ 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
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
- ) -> None:
+ 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.
+ 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
+ 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}{switch.func}>"
- f"{switch.func_type}>"
- )
- await self.call_request(uri, method="put", data=data)
+ 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
+
+ if switched > 0:
+ return 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."""
diff --git a/pyproject.toml b/pyproject.toml
index ddf1ba51a..ede4e456a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,10 @@
[build-system]
-requires = ["setuptools~=79.0"]
+requires = ["setuptools~=80.0"]
build-backend = "setuptools.build_meta"
[project]
name = "plugwise"
-version = "1.7.4"
+version = "1.7.6"
license = "MIT"
description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3."
readme = "README.md"
@@ -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/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/complexity.sh b/scripts/complexity.sh
index fbae27090..8089723c9 100755
--- a/scripts/complexity.sh
+++ b/scripts/complexity.sh
@@ -3,18 +3,23 @@ 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
+ # shellcheck disable=SC1091 # ingesting virtualenv
+ . "${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
+ # shellcheck disable=SC1090 # ingesting virtualenv
+ . "${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/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
diff --git a/scripts/run-in-env.sh b/scripts/run-in-env.sh
index 624d5ea95..946cbb3e5 100755
--- a/scripts/run-in-env.sh
+++ b/scripts/run-in-env.sh
@@ -1,19 +1,33 @@
-#!/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
+ # shellcheck disable=SC1091 # ingesting virtualenv
+ . "${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
+ # shellcheck disable=SC1090 # ingesting virtualenv
+ . "${my_path}/${venv}/bin/activate"
+ break
+ fi
+ done
fi
+
+exec "$@"
diff --git a/scripts/setup.sh b/scripts/setup.sh
index add58c35b..00073f8d6 100755
--- a/scripts/setup.sh
+++ b/scripts/setup.sh
@@ -1,23 +1,23 @@
#!/usr/bin/env bash
-set -eu
+# 20250613 Copied from HA-core and shell-check adjusted and modified for local use
+set -e
-my_path=$(git rev-parse --show-toplevel)
-
-# shellcheck disable=SC1091
-. "${my_path}/scripts/python-venv.sh"
+if [ -z "$VIRTUAL_ENV" ]; then
+ if [ -x "$(command -v uv)" ]; then
+ uv venv venv
+ else
+ python3 -m venv venv
+ fi
+ # shellcheck disable=SC1091 # ingesting virtualenv
+ 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..2120fe14c 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 shell-check adjusted and modified for local use
+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
-else
- echo "Virtualenv available, bailing out"
- exit 2
+if [ -z "$VIRTUAL_ENV" ]; then
+ if [ -x "$(command -v uv)" ]; then
+ uv venv venv
+ else
+ python3 -m venv venv
+ fi
+ # shellcheck disable=SC1091 # ingesting virtualenv
+ 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"
+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"
+
+# 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..f4a1b0742 100755
--- a/scripts/tests_and_coverage.sh
+++ b/scripts/tests_and_coverage.sh
@@ -1,24 +1,40 @@
#!/usr/bin/env bash
+# 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
+ # shellcheck disable=SC1091 # ingesting virtualenv
+ . "${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
+ # shellcheck disable=SC1090 # ingesting virtualenv
+ . "${my_path}/${venv}/bin/activate"
+ break
+ fi
+ done
+fi
+
+# 20250613 End of copy
+
+if ! command -v pytest >/dev/null; then
+ echo "Unable to find pytest, run setup_test.sh before this script"
+ exit 1
fi
handle_command_error() {
@@ -36,7 +52,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
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
diff --git a/tests/data/adam/adam_multiple_devices_per_zone.json b/tests/data/adam/adam_multiple_devices_per_zone.json
index 523f9cfcc..2780afe76 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..96b015ce5 100644
--- a/tests/test_adam.py
+++ b/tests/test_adam.py
@@ -106,14 +106,27 @@ 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"
)
assert not switch_change
+ switch_change = await self.tinker_switch(
+ smile,
+ "2568cc4b9c1e401495d4741a5f89bee1",
+ model="lock",
+ )
+ assert switch_change
+
+ assert await self.tinker_switch_bad_input(
+ smile,
+ "854f8a9b0e7e425db97f1f110e1ce4b3",
+ )
tinkered = await self.tinker_gateway_mode(smile)
assert not tinkered
@@ -288,7 +301,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
@@ -304,6 +317,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/tests/test_init.py b/tests/test_init.py
index 0369f6503..c80468ec6 100644
--- a/tests/test_init.py
+++ b/tests/test_init.py
@@ -684,16 +684,18 @@ 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 = {"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:
- await smile.set_switch_state(dev_id, members, model, new_state)
- tinker_switch_passed = True
- _LOGGER.info(" + tinker_switch worked as intended")
- except pw_exceptions.PlugwiseError:
- _LOGGER.info(" + locked, not switched as expected")
- return False
+ 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")
+ else:
+ _LOGGER.info(" + tinker_switch failed unexpectedly")
+ return False
except (
pw_exceptions.ConnectionFailedError
): # leave for-loop at connect-error
@@ -706,6 +708,20 @@ 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
+ ):
+ """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:
+ 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
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
+
+
+