Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

- Change representation of no-thermostat-schedule-defined to a single `off` option via PR [#899](https://github.com/plugwise/python-plugwise/pull/899)

## v1.14.2

- Implement common typing in functions that are interacting.

## v1.14.1

- Improve manual fixtures script, reorder set_schedule_state() arguments for better compatibility with set_dhw_mode(), via PR[#897](https://github.com/plugwise/python-plugwise/pull/897)
Expand Down
12 changes: 4 additions & 8 deletions plugwise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ async def set_select(
key: str,
loc_id: str,
option: str,
state: str | None = None,
state: int | str | None = None,
) -> None:
"""Set the selected option for the applicable Select."""
try:
Expand All @@ -372,7 +372,7 @@ async def set_schedule_state(
self,
loc_id: str,
name: str | None = None,
state: str | None = None,
state: int | str | None = None,
) -> None:
"""Activate/deactivate the Schedule, with the given name, on the relevant Thermostat."""
try:
Expand Down Expand Up @@ -462,19 +462,15 @@ async def set_regulation_mode(self, mode: str) -> None:
) from exc # pragma no cover

async def set_dhw_mode(
self,
key: str,
location: str,
mode: str,
length: int,
self, key: str, location: str, mode: str, length: int | str | None = None
) -> None:
"""Set the domestic hot water heating regulation mode."""
try: # pragma no cover
await self._smile_api.set_dhw_mode(
key,
location,
mode,
length,
length=length,
) # pragma: no cover
except ConnectionFailedError as exc: # pragma no cover
raise ConnectionFailedError(
Expand Down
6 changes: 3 additions & 3 deletions plugwise/legacy/smile.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ async def set_dhw_mode(
key: str,
location: str,
mode: str,
length: int,
length: int | str | None,
) -> None:
"""Set-function placeholder for legacy devices."""

Expand Down Expand Up @@ -178,7 +178,7 @@ async def set_select(
_: str,
loc_id: str,
option: str,
state: str | None = None,
state: int | str | None = None,
) -> None:
"""Set the thermostat schedule option."""
# schedule name corresponds to select option
Expand All @@ -188,7 +188,7 @@ async def set_schedule_state(
self,
_: str,
name: str | None = None,
state: str | None = None,
state: int | str | None = None,
) -> None:
"""Activate/deactivate the Schedule.

Expand Down
15 changes: 10 additions & 5 deletions plugwise/smile.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ async def set_select(
key: str,
appl_or_loc_id: str,
option: str,
state: str | None = None,
state: int | str | None = None,
) -> None:
"""Set a dhw/gateway/regulation mode or the thermostat schedule option."""
match key:
Expand All @@ -257,16 +257,21 @@ async def set_select(
await self.set_zone_profile(appl_or_loc_id, option)

async def set_dhw_mode(
self, key: str, appl_id: str, mode: str, length: int
self, key: str, appl_id: str, mode: str, length: int | str | None = None
) -> None:
"""Set the domestic hot water mode.

Two options are known:
- 2 modes, comfort and off, representing the dhw comfort mode on and off switch states,
- and the 5 modes available on the Loria.
"""
if self._dhw_allowed_modes and mode not in self._dhw_allowed_modes:
raise PlugwiseError("Plugwise: invalid dhw mode.")
if (
self._dhw_allowed_modes
and mode not in self._dhw_allowed_modes
or length is None
or not isinstance(length, int)
):
raise PlugwiseError("Plugwise: invalid dhw mode or invalid dhw modes list.")
Comment on lines +260 to +274

@coderabbitai coderabbitai Bot Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)plugwise/(smile\.py|__init__\.py)$' || true

echo "== relevant smile.py section =="
sed -n '220,290p' plugwise/smile.py

echo "== relevant __init__.py section =="
sed -n '430,490p' plugwise/__init__.py

echo "== dhw_allowed_modes and dhw method usages =="
rg -n "_dhw_allowed_modes|dhw_allowed_modes|set_dhw_mode|dh[ _]modes|length" plugwise -S

Repository: plugwise/python-plugwise

Length of output: 7019


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== helper.py dhw mode sections =="
sed -n '70,100p' plugwise/helper.py
sed -n '250,290p' plugwise/helper.py
sed -n '355,375p' plugwise/helper.py
sed -n '480,520p' plugwise/helper.py

echo "== tests mentioning dhw mode/length =="
git ls-files | rg -i 'test.*dhw|dhw.*test|smile|plugwise' | sed -n '1,120p'
rg -n "dhw_mode|dhw_modes|select_dhw_mode|length.*dhw|domestic_hot_water|domestic_hot_water_mode_control_functionality" . -S --glob '!*.pyc'

Repository: plugwise/python-plugwise

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== __init__.py docstring around set_dhw_mode with duration mention =="
rg -n -A 8 -B 8 "Set the domestic hot water heating regulation mode|duration|int| str | None = None" plugwise/__init__.py

echo "== legacy set_dhw_mode for comparison =="
sed -n '120,170p' plugwise/legacy/smile.py

echo "== behavioral model of current validation for missing allowed modes and string/integer lengths =="
python3 - <<'PY'
def current_validation(dhw_allowed_modes, mode, length):
    return (
        dhw_allowed_modes
        and mode not in dhw_allowed_modes
        or length is None
        or not isinstance(length, int)
    )

api_path_allowed = True
api_textual_accepted = False

cases = [
    ("no dhw metadata / mode='comfort' / length=2", None, "comfort", 2),
    ("no dhw metadata / mode='invalid' / length=2", None, "invalid", 2),
    ("no dhw metadata / mode='comfort' / length='short'", None, "comfort", "short"),
    ("lora modes / mode='invalid' / length=2", ["auto", "eco"], "invalid", 2),
    ("lora modes / mode='auto' / length='short'", ["auto", "eco"], "auto", "short"),
    ("supported length not two routes differently / length=3", ["comfort", "eco"], "comfort", 3),
]
for name, dhw_allowed_modes, mode, length in cases:
    print(f"{name}: core_rejected={current_validation(dhw_allowed_modes, mode, length)}")
PY

Repository: plugwise/python-plugwise

Length of output: 10980


Ensure DHW mode validation reaches the device layer.

plugwise/__init__.py allows textual length values, while plugwise/smile.py only rejects text and passes any integer through; when _dhw_allowed_modes is unavailable, invalid mode values bypass rejection on the length != 2 path. Require a supported mode when explicit modes are known and align the public wrapper’s length contract with the accepted values.

📍 Affects 2 files
  • plugwise/smile.py#L260-L274 (this comment)
  • plugwise/__init__.py#L464-L473
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugwise/smile.py` around lines 260 - 274, Update the DHW setter validation
in the relevant smile.py method to always reject unsupported mode values when
_dhw_allowed_modes is available, while requiring length to be one of the
device-supported integer values. Align the public wrapper in
plugwise/__init__.py with this contract by accepting only the supported length
representations and forwarding them consistently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignored for now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bouwew, acknowledged. I’ll leave this finding unresolved for now. Would you like me to open a follow-up issue for the DHW validation and wrapper-contract alignment?

🐇💭


match length:
case 2:
Expand Down Expand Up @@ -346,7 +351,7 @@ async def set_zone_profile(self, loc_id: str, profile: str) -> None:
await self.call_request(uri, method="post", data=data)

async def set_schedule_state(
self, loc_id: str, name: str | None = None, state: str | None = None
self, loc_id: str, name: str | None = None, state: int | str | None = None
) -> None:
"""Activate/deactivate the Schedule, with the given name, on the relevant Thermostat.

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "plugwise"
version = "1.14.1"
version = "1.14.2"
license = "MIT"
description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3."
readme = "README.md"
Expand Down