diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bc509653c..0612278bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: args: - --safe - --quiet - files: ^((plugwise|tests)/.+)?[^/]+\.py$ + files: ^((plugwise|tests|scripts)/.+)?[^/]+\.py$ # Moved codespell configuration to setup.cfg as per 'all-files' issues not reading args - repo: https://github.com/codespell-project/codespell rev: v2.2.4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 461a65ed8..da575018e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## v0.31.3: Typing updates, improved fixture generation and manual mode-changes + ## v0.31.2: Introduce strict-typing (py.typed) ## v0.31.1: Legacy Anna - read and process system-xml data diff --git a/plugwise/constants.py b/plugwise/constants.py index 7e9139521..1c1a9b830 100644 --- a/plugwise/constants.py +++ b/plugwise/constants.py @@ -40,6 +40,7 @@ ) ACTIVE_ACTUATORS: Final[tuple[str, ...]] = ( "domestic_hot_water_setpoint", + "max_dhw_temperature", "maximum_boiler_temperature", "thermostat", ) @@ -299,7 +300,6 @@ "humidity", "illuminance", "intended_boiler_temperature", - "maximum_boiler_temperature", "modulation_level", "net_electricity_cumulative", "net_electricity_point", @@ -383,6 +383,7 @@ class SmileSensors(TypedDict, total=False): cooling_activation_outdoor_temperature: float cooling_deactivation_threshold: float dhw_temperature: float + domestic_hot_water_setpoint: float temperature: float electricity_consumed: float electricity_consumed_interval: float @@ -500,7 +501,7 @@ class DeviceData( available: bool | None binary_sensors: SmileBinarySensors - domestic_hot_water_setpoint: ActuatorData | float + max_dhw_temperature: ActuatorData | float maximum_boiler_temperature: ActuatorData | float sensors: SmileSensors switches: SmileSwitches diff --git a/plugwise/helper.py b/plugwise/helper.py index 88655b7e2..816b3ab2b 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -117,6 +117,12 @@ def _get_actuator_functionalities(xml: etree, data: DeviceData) -> None: temp_dict[key] = format_measure(function.text, TEMP_CELSIUS) # type: ignore [literal-required] if temp_dict: + if item == "domestic_hot_water_setpoint": + item = "max_dhw_temperature" + # If max_dhw_temperature is present, don't show domestic_hot_water_setpoint sensor + if "domestic_hot_water_setpoint" in data: + data.pop("domestic_hot_water_setpoint") + data[item] = temp_dict # type: ignore [literal-required] diff --git a/pyproject.toml b/pyproject.toml index be53743e7..2af9e3447 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plugwise" -version = "0.31.2" +version = "0.31.3" license = {file = "LICENSE"} description = "Plugwise Smile (Adam/Anna/P1) and Stretch module for Python 3." readme = "README.md" diff --git a/scripts/fake_fixtures.py b/scripts/fake_fixtures.py new file mode 100755 index 000000000..fe196b3bd --- /dev/null +++ b/scripts/fake_fixtures.py @@ -0,0 +1,405 @@ +#!/usr/bin/env python3 +"""Generate fake fixtures from existing fixtures.""" + +import json +import os + +print("... Crafting m_* fixtures from userdata ...") # noqa: T201 + +### ADAM + +base_adam_fake = "adam_plus_anna_new" +basefile = f"./fixtures/{base_adam_fake}/all_data.json" +basefile_n = f"./fixtures/{base_adam_fake}/notifications.json" + +io = open(basefile) +base = json.load(io) +io_n = open(basefile_n) +base_n = json.load(io_n) + +m_adam_cooling = base.copy() + +# Set cooling_present to true +m_adam_cooling["gateway"]["cooling_present"] = True + +# Remove device "67d73d0bd469422db25a618a5fb8eeb0" from anywhere +m_adam_cooling["devices"].pop("67d73d0bd469422db25a618a5fb8eeb0") + +# Remove setpoint for "ad4838d7d35c4d6ea796ee12ae5aedf8" and inject low and high +m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["thermostat"].pop( + "setpoint" +) +m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["thermostat"][ + "setpoint_low" +] = 4.0 +m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["thermostat"][ + "setpoint_high" +] = 23.5 + +# Add new key available +m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["available"] = True + +m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"][ + "selected_schedule" +] = "None" +m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"][ + "control_state" +] = "cooling" +m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["mode"] = "heat_cool" + +# (following diff, now 2954 is removed) +# Remove device "29542b2b6a6a4169acecc15c72a599b8" from anywhere +m_adam_cooling["devices"].pop("29542b2b6a6a4169acecc15c72a599b8") + +# Back at ad48 +m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"].pop("setpoint") +m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"][ + "temperature" +] = 25.8 +m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"][ + "setpoint_low" +] = 4.0 +m_adam_cooling["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"][ + "setpoint_high" +] = 23.5 + +# (again, following diff) +# Remove device "2568cc4b9c1e401495d4741a5f89bee1" from anywhere +m_adam_cooling["devices"].pop("2568cc4b9c1e401495d4741a5f89bee1") +# Remove device "854f8a9b0e7e425db97f1f110e1ce4b3" from anywhere +m_adam_cooling["devices"].pop("854f8a9b0e7e425db97f1f110e1ce4b3") + +# Go for 1772 +m_adam_cooling["devices"]["1772a4ea304041adb83f357b751341ff"]["sensors"].pop("setpoint") +m_adam_cooling["devices"]["1772a4ea304041adb83f357b751341ff"]["sensors"][ + "temperature" +] = 21.6 + +# Go for e2f4 +m_adam_cooling["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["thermostat"].pop( + "setpoint" +) +m_adam_cooling["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["thermostat"][ + "setpoint_low" +] = 19.0 +m_adam_cooling["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["thermostat"][ + "setpoint_high" +] = 25.0 + +m_adam_cooling["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"].pop("setpoint") +m_adam_cooling["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"][ + "temperature" +] = 239 +# didn't change +# m_adam_cooling["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"]["battery"] = 56 +m_adam_cooling["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"][ + "setpoint_low" +] = 20.0 +m_adam_cooling["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"][ + "setpoint_high" +] = 23.5 + + +# Go for da22 +m_adam_cooling["devices"]["da224107914542988a88561b4452b0f6"][ + "regulation_mode" +] = "cooling" +m_adam_cooling["devices"]["da224107914542988a88561b4452b0f6"][ + "regulation_modes" +].append("cooling") +m_adam_cooling["devices"]["da224107914542988a88561b4452b0f6"]["sensors"][ + "outdoor_temperature" +] = 29.65 + +# Go for 056e +m_adam_cooling["devices"]["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ + "cooling_state" +] = True +m_adam_cooling["devices"]["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ + "heating_state" +] = False +m_adam_cooling["devices"]["056ee145a816487eaa69243c3280f8bf"]["sensors"][ + "water_temperature" +] = 19.0 +m_adam_cooling["devices"]["056ee145a816487eaa69243c3280f8bf"]["sensors"][ + "intended_boiler_temperature" +] = 17.5 + +fake_name = "m_adam_cooling" + +if not os.path.exists(f"./fixtures/{fake_name}"): + os.makedirs(f"./fixtures/{fake_name}") + +outfile = f"./fixtures/{fake_name}/all_data.json" +data = json.dumps(m_adam_cooling, indent=2) +with open(outfile, "w") as f: + f.write(data) + +outfile = f"./fixtures/{fake_name}/notifications.json" +data = json.dumps(base_n, indent=2) +with open(outfile, "w") as f: + f.write(data) + + +### FROM ABOVE + +m_adam_heating = m_adam_cooling.copy() + +# Set cooling_present to true +m_adam_cooling["gateway"]["cooling_present"] = False + +# Remove setpoint for "ad4838d7d35c4d6ea796ee12ae5aedf8" and inject low and high +m_adam_heating["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["thermostat"][ + "setpoint" +] = 20.0 +m_adam_heating["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["thermostat"].pop( + "setpoint_low" +) +m_adam_heating["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["thermostat"].pop( + "setpoint_high" +) + +m_adam_heating["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"][ + "control_state" +] = "heating" +m_adam_heating["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["mode"] = "heat" + +# Back at ad48 +m_adam_heating["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"][ + "setpoint" +] = 20.0 +m_adam_heating["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"][ + "temperature" +] = 19.1 +m_adam_heating["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"].pop( + "setpoint_low" +) +m_adam_heating["devices"]["ad4838d7d35c4d6ea796ee12ae5aedf8"]["sensors"].pop( + "setpoint_high" +) + +# Go for 1772 +m_adam_heating["devices"]["1772a4ea304041adb83f357b751341ff"]["sensors"][ + "temperature" +] = 18.6 + +# Go for e2f4 +m_adam_heating["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["thermostat"][ + "setpoint" +] = 15.0 +m_adam_heating["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["thermostat"].pop( + "setpoint_low" +) +m_adam_heating["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["thermostat"].pop( + "setpoint_high" +) + +m_adam_heating["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"][ + "setpoint" +] = 15.0 +m_adam_heating["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"][ + "temperature" +] = 17.9 +# m_adam_heating["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"]["battery"] = 56 +m_adam_heating["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"].pop( + "setpoint_low" +) +m_adam_heating["devices"]["e2f4322d57924fa090fbbc48b3a140dc"]["sensors"].pop( + "setpoint_high" +) + + +# Go for da22 +m_adam_heating["devices"]["da224107914542988a88561b4452b0f6"][ + "regulation_mode" +] = "heating" +m_adam_heating["devices"]["da224107914542988a88561b4452b0f6"][ + "regulation_modes" +].remove("cooling") +m_adam_heating["devices"]["da224107914542988a88561b4452b0f6"]["sensors"][ + "outdoor_temperature" +] = -1.25 + +# Go for 056e +m_adam_heating["devices"]["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"].pop( + "cooling_state" +) +m_adam_heating["devices"]["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ + "heating_state" +] = True +m_adam_heating["devices"]["056ee145a816487eaa69243c3280f8bf"]["sensors"][ + "water_temperature" +] = 37.0 +m_adam_heating["devices"]["056ee145a816487eaa69243c3280f8bf"]["sensors"][ + "intended_boiler_temperature" +] = 38.1 +m_adam_heating["devices"]["056ee145a816487eaa69243c3280f8bf"][ + "domestic_hot_water_setpoint" +] = {"setpoint": 60.0, "lower_bound": 25.0, "upper_bound": 95.0, "resolution": 0.01} + +fake_name = "m_adam_heating" + +if not os.path.exists(f"./fixtures/{fake_name}"): + os.makedirs(f"./fixtures/{fake_name}") + +outfile = f"./fixtures/{fake_name}/all_data.json" +data = json.dumps(m_adam_heating, indent=2) +with open(outfile, "w") as f: + f.write(data) + +outfile = f"./fixtures/{fake_name}/notifications.json" +data = json.dumps(base_n, indent=2) +with open(outfile, "w") as f: + f.write(data) + +### ANNA + +base_anna_fake = "anna_heatpump_heating" +basefile = f"./fixtures/{base_anna_fake}/all_data.json" +basefile_n = f"./fixtures/{base_anna_fake}/notifications.json" + +io = open(basefile) +base = json.load(io) +io_n = open(basefile_n) +base_n = json.load(io_n) + +m_anna_heatpump_cooling = base.copy() + +# Set cooling_present to true +m_anna_heatpump_cooling["gateway"]["cooling_present"] = True + +# Go for 1cbf +m_anna_heatpump_cooling["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"][ + "model" +] = "Generic heater/cooler" + +m_anna_heatpump_cooling["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"][ + "binary_sensors" +]["cooling_enabled"] = True +m_anna_heatpump_cooling["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"][ + "binary_sensors" +]["heating_state"] = False +m_anna_heatpump_cooling["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"][ + "binary_sensors" +]["cooling_state"] = True + +m_anna_heatpump_cooling["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "water_temperature" +] = 22.7 +m_anna_heatpump_cooling["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "dhw_temperature" +] = 41.5 +m_anna_heatpump_cooling["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "intended_boiler_temperature" +] = 0.0 +m_anna_heatpump_cooling["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "modulation_level" +] = 40 +m_anna_heatpump_cooling["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "return_temperature" +] = 23.8 +m_anna_heatpump_cooling["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "outdoor_air_temperature" +] = 28.0 + + +# Go for 015a +m_anna_heatpump_cooling["devices"]["015ae9ea3f964e668e490fa39da3870b"]["sensors"][ + "outdoor_temperature" +] = 28.2 + +# Go for 3cb7 +m_anna_heatpump_cooling["devices"]["3cb70739631c4d17a86b8b12e8a5161b"][ + "thermostat" +].pop("setpoint") +m_anna_heatpump_cooling["devices"]["3cb70739631c4d17a86b8b12e8a5161b"]["thermostat"][ + "setpoint_low" +] = 20.5 +m_anna_heatpump_cooling["devices"]["3cb70739631c4d17a86b8b12e8a5161b"]["thermostat"][ + "setpoint_high" +] = 24.0 + +m_anna_heatpump_cooling["devices"]["3cb70739631c4d17a86b8b12e8a5161b"]["sensors"].pop( + "setpoint" +) +m_anna_heatpump_cooling["devices"]["3cb70739631c4d17a86b8b12e8a5161b"]["sensors"][ + "temperature" +] = 26.3 +m_anna_heatpump_cooling["devices"]["3cb70739631c4d17a86b8b12e8a5161b"]["sensors"][ + "setpoint_low" +] = 20.5 +m_anna_heatpump_cooling["devices"]["3cb70739631c4d17a86b8b12e8a5161b"]["sensors"][ + "setpoint_high" +] = 24.0 + +fake_name = "m_anna_heatpump_cooling" + +if not os.path.exists(f"./fixtures/{fake_name}"): + os.makedirs(f"./fixtures/{fake_name}") + +outfile = f"./fixtures/{fake_name}/all_data.json" +data = json.dumps(m_anna_heatpump_cooling, indent=2) +with open(outfile, "w") as f: + f.write(data) + +outfile = f"./fixtures/{fake_name}/notifications.json" +data = json.dumps(base_n, indent=2) +with open(outfile, "w") as f: + f.write(data) + + +### FROM ABOVE + +m_anna_heatpump_idle = m_anna_heatpump_cooling.copy() + +# Go for 1cbf +m_anna_heatpump_idle["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["binary_sensors"][ + "compressor_state" +] = False +m_anna_heatpump_idle["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["binary_sensors"][ + "cooling_state" +] = False + +m_anna_heatpump_idle["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "water_temperature" +] = 19.1 +m_anna_heatpump_idle["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "dhw_temperature" +] = 46.3 +m_anna_heatpump_idle["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "intended_boiler_temperature" +] = 18.0 +m_anna_heatpump_idle["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "modulation_level" +] = 0 +m_anna_heatpump_idle["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "return_temperature" +] = 22.0 +m_anna_heatpump_idle["devices"]["1cbf783bb11e4a7c8a6843dee3a86927"]["sensors"][ + "outdoor_air_temperature" +] = 28.2 + + +# Go for 3cb7 + +m_anna_heatpump_idle["devices"]["3cb70739631c4d17a86b8b12e8a5161b"]["sensors"][ + "temperature" +] = 23.0 +m_anna_heatpump_idle["devices"]["3cb70739631c4d17a86b8b12e8a5161b"]["sensors"][ + "cooling_activation_outdoor_temperature" +] = 25.0 + +fake_name = "m_anna_heatpump_idle" + +if not os.path.exists(f"./fixtures/{fake_name}"): + os.makedirs(f"./fixtures/{fake_name}") + +outfile = f"./fixtures/{fake_name}/all_data.json" +data = json.dumps(m_anna_heatpump_idle, indent=2) +with open(outfile, "w") as f: + f.write(data) + +outfile = f"./fixtures/{fake_name}/notifications.json" +data = json.dumps(base_n, indent=2) +with open(outfile, "w") as f: + f.write(data) diff --git a/scripts/tests_and_coverage.sh b/scripts/tests_and_coverage.sh index 5cd60225d..801e44242 100755 --- a/scripts/tests_and_coverage.sh +++ b/scripts/tests_and_coverage.sh @@ -36,4 +36,7 @@ if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "linting" ] ; then echo "... pylint-ing ..." pylint plugwise/ tests/ + + echo "... crafting fake_fixtures ..." + PYTHONPATH=$(pwd) python3 scripts/fake_fixtures.py fi diff --git a/tests/test_smile.py b/tests/test_smile.py index 3e428a767..f2badfbda 100644 --- a/tests/test_smile.py +++ b/tests/test_smile.py @@ -385,7 +385,7 @@ async def device_test(self, smile=pw_smile.Smile, testdata=None): if "heater_id" in data.gateway: self.cooling_present = data.gateway["cooling_present"] self.notifications = data.gateway["notifications"] - self._write_json("all_data", [data.gateway, data.devices]) + self._write_json("all_data", {"gateway": data.gateway, "devices": data.devices}) self._write_json("notifications", data.gateway["notifications"]) location_list = smile._thermo_locs @@ -987,7 +987,7 @@ async def test_connect_anna_v4(self): "upper_bound": 100.0, "resolution": 1.0, }, - "domestic_hot_water_setpoint": { + "max_dhw_temperature": { "setpoint": 60.0, "lower_bound": 30.0, "upper_bound": 60.0, @@ -1104,7 +1104,7 @@ async def test_connect_anna_v4_dhw(self): "upper_bound": 100.0, "resolution": 1.0, }, - "domestic_hot_water_setpoint": { + "max_dhw_temperature": { "setpoint": 60.0, "lower_bound": 30.0, "upper_bound": 60.0, @@ -2735,7 +2735,7 @@ async def test_adam_heatpump_cooling(self): "upper_bound": 50.0, "resolution": 0.01, }, - "domestic_hot_water_setpoint": { + "max_dhw_temperature": { "setpoint": 60.0, "lower_bound": 40.0, "upper_bound": 65.0, @@ -3275,7 +3275,7 @@ async def test_adam_plus_jip(self): "upper_bound": 90.0, "resolution": 0.01, }, - "domestic_hot_water_setpoint": { + "max_dhw_temperature": { "setpoint": 60.0, "lower_bound": 40.0, "upper_bound": 60.0, @@ -4311,7 +4311,7 @@ async def test_connect_anna_loria_heating_idle(self): "upper_bound": 45.0, "resolution": 0.01, }, - "domestic_hot_water_setpoint": { + "max_dhw_temperature": { "setpoint": 53.0, "lower_bound": 35.0, "upper_bound": 60.0, @@ -4419,7 +4419,7 @@ async def test_connect_anna_loria_cooling_active(self): "upper_bound": 45.0, "resolution": 0.01, }, - "domestic_hot_water_setpoint": { + "max_dhw_temperature": { "setpoint": 53.0, "lower_bound": 35.0, "upper_bound": 60.0,