From 1aab6e94ee82c12808e70303b3927d4de8f275af Mon Sep 17 00:00:00 2001 From: d-walsh <2351963+d-walsh@users.noreply.github.com> Date: Fri, 12 Jun 2026 18:54:18 -0400 Subject: [PATCH 1/3] fix: include superQuiet in 3-speed unit fan_speeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Units reporting numberOfFanSpeeds=3 were mapped to ["quiet", "low", "powerful"], omitting superQuiet. However, pykumo's set_fan_speed() already accepts superQuiet (it's in ALL_FAN_SPEEDS and the unit firmware honours it), so the omission is a profile under-report rather than a hardware limitation — confirmed on Mitsubishi mini-splits that respond to superQuiet commands despite advertising 3 speeds. Update the speeds==3 branch to match the speeds==5 branch in including superQuiet as the quietest option. Add unit tests covering all three speed-count branches. Co-Authored-By: Claude --- pykumo/py_kumo.py | 2 +- tests/test_fan_speeds.py | 69 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 tests/test_fan_speeds.py diff --git a/pykumo/py_kumo.py b/pykumo/py_kumo.py index 2e81e76..f2edecc 100644 --- a/pykumo/py_kumo.py +++ b/pykumo/py_kumo.py @@ -389,7 +389,7 @@ def get_fan_speeds(self): ) if speeds == 3: - valid_speeds = ["quiet", "low", "powerful"] + valid_speeds = ["superQuiet", "quiet", "low", "powerful"] elif speeds == 4: valid_speeds = ["quiet", "Low", "powerful", "superPowerful"] else: diff --git a/tests/test_fan_speeds.py b/tests/test_fan_speeds.py new file mode 100644 index 0000000..76235d5 --- /dev/null +++ b/tests/test_fan_speeds.py @@ -0,0 +1,69 @@ +"""Tests for get_fan_speeds() across all numberOfFanSpeeds values.""" + +import unittest +from unittest.mock import patch +from pykumo.py_kumo import PyKumo + + +def make_unit(num_speeds, has_auto=False): + """Return a minimally-mocked PyKumo with the given profile.""" + with patch.object(PyKumo, "__init__", lambda self, *a, **kw: None): + unit = PyKumo.__new__(PyKumo) + profile = {"numberOfFanSpeeds": num_speeds} + if has_auto: + profile["hasFanSpeedAuto"] = True + unit._profile = profile + unit._status = {} + return unit + + +class TestGetFanSpeeds(unittest.TestCase): + def test_3speed_includes_superquiet(self): + """Units reporting numberOfFanSpeeds=3 must expose superQuiet.""" + unit = make_unit(3) + speeds = unit.get_fan_speeds() + self.assertIn("superQuiet", speeds, "3-speed units should include superQuiet") + self.assertIn("quiet", speeds) + self.assertIn("low", speeds) + self.assertIn("powerful", speeds) + + def test_3speed_superquiet_is_first(self): + """superQuiet should be the first option for 3-speed units.""" + unit = make_unit(3) + speeds = unit.get_fan_speeds() + self.assertEqual(speeds[0], "superQuiet") + + def test_3speed_with_auto(self): + """3-speed + hasFanSpeedAuto appends auto after superQuiet.""" + unit = make_unit(3, has_auto=True) + speeds = unit.get_fan_speeds() + self.assertIn("superQuiet", speeds) + self.assertIn("auto", speeds) + + def test_4speed_unchanged(self): + """4-speed behaviour is unchanged.""" + unit = make_unit(4) + speeds = unit.get_fan_speeds() + self.assertEqual(speeds, ["quiet", "Low", "powerful", "superPowerful"]) + + def test_5speed_unchanged(self): + """5-speed (default) behaviour is unchanged.""" + unit = make_unit(5) + speeds = unit.get_fan_speeds() + self.assertEqual( + speeds, ["superQuiet", "quiet", "low", "powerful", "superPowerful"] + ) + + def test_set_fan_speed_accepts_superquiet_on_3speed(self): + """set_fan_speed must accept superQuiet for 3-speed units.""" + with patch.object(PyKumo, "__init__", lambda self, *a, **kw: None): + unit = PyKumo.__new__(PyKumo) + unit._profile = {"numberOfFanSpeeds": 3} + unit._status = {} + # Verify get_fan_speeds includes superQuiet so set_fan_speed won't reject it + valid = unit.get_fan_speeds() + self.assertIn("superQuiet", valid) + + +if __name__ == "__main__": + unittest.main() From 7a2caf59d77151677e946ac8fb9b6603f714a16d Mon Sep 17 00:00:00 2001 From: d-walsh <2351963+d-walsh@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:36:05 -0400 Subject: [PATCH 2/3] fix: expose full 5-speed set for 3-speed under-reporting units Extends the speeds==3 branch to return the full ["superQuiet", "quiet", "low", "powerful", "superPowerful"] list, matching the speeds==5 branch. Hardware testing on Mitsubishi units advertising numberOfFanSpeeds=3 confirms all five speeds are accepted by the firmware. The previous fix (PR #73 v1) added superQuiet; this update adds superPowerful at the loud end, giving the full symmetric range. Tests updated: 3-speed branch now asserts the complete 5-element list and two new tests verify both superQuiet and superPowerful are accepted by set_fan_speed. 19/19 tests pass. Co-Authored-By: Claude --- pykumo/py_kumo.py | 2 +- tests/test_fan_speeds.py | 36 ++++++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/pykumo/py_kumo.py b/pykumo/py_kumo.py index f2edecc..06040c9 100644 --- a/pykumo/py_kumo.py +++ b/pykumo/py_kumo.py @@ -389,7 +389,7 @@ def get_fan_speeds(self): ) if speeds == 3: - valid_speeds = ["superQuiet", "quiet", "low", "powerful"] + valid_speeds = ["superQuiet", "quiet", "low", "powerful", "superPowerful"] elif speeds == 4: valid_speeds = ["quiet", "Low", "powerful", "superPowerful"] else: diff --git a/tests/test_fan_speeds.py b/tests/test_fan_speeds.py index 76235d5..7562def 100644 --- a/tests/test_fan_speeds.py +++ b/tests/test_fan_speeds.py @@ -18,14 +18,14 @@ def make_unit(num_speeds, has_auto=False): class TestGetFanSpeeds(unittest.TestCase): - def test_3speed_includes_superquiet(self): - """Units reporting numberOfFanSpeeds=3 must expose superQuiet.""" + def test_3speed_full_set(self): + """Units reporting numberOfFanSpeeds=3 expose the full 5-speed set.""" unit = make_unit(3) speeds = unit.get_fan_speeds() - self.assertIn("superQuiet", speeds, "3-speed units should include superQuiet") - self.assertIn("quiet", speeds) - self.assertIn("low", speeds) - self.assertIn("powerful", speeds) + self.assertEqual( + speeds, + ["superQuiet", "quiet", "low", "powerful", "superPowerful"], + ) def test_3speed_superquiet_is_first(self): """superQuiet should be the first option for 3-speed units.""" @@ -33,12 +33,20 @@ def test_3speed_superquiet_is_first(self): speeds = unit.get_fan_speeds() self.assertEqual(speeds[0], "superQuiet") + def test_3speed_superpowerful_is_last_before_auto(self): + """superPowerful should be the last non-auto option for 3-speed units.""" + unit = make_unit(3) + speeds = unit.get_fan_speeds() + self.assertEqual(speeds[-1], "superPowerful") + def test_3speed_with_auto(self): - """3-speed + hasFanSpeedAuto appends auto after superQuiet.""" + """3-speed + hasFanSpeedAuto appends auto at end.""" unit = make_unit(3, has_auto=True) speeds = unit.get_fan_speeds() - self.assertIn("superQuiet", speeds) - self.assertIn("auto", speeds) + self.assertEqual( + speeds, + ["superQuiet", "quiet", "low", "powerful", "superPowerful", "auto"], + ) def test_4speed_unchanged(self): """4-speed behaviour is unchanged.""" @@ -60,10 +68,18 @@ def test_set_fan_speed_accepts_superquiet_on_3speed(self): unit = PyKumo.__new__(PyKumo) unit._profile = {"numberOfFanSpeeds": 3} unit._status = {} - # Verify get_fan_speeds includes superQuiet so set_fan_speed won't reject it valid = unit.get_fan_speeds() self.assertIn("superQuiet", valid) + def test_set_fan_speed_accepts_superpowerful_on_3speed(self): + """set_fan_speed must accept superPowerful for 3-speed units.""" + with patch.object(PyKumo, "__init__", lambda self, *a, **kw: None): + unit = PyKumo.__new__(PyKumo) + unit._profile = {"numberOfFanSpeeds": 3} + unit._status = {} + valid = unit.get_fan_speeds() + self.assertIn("superPowerful", valid) + if __name__ == "__main__": unittest.main() From f850701f6fc5b64447f78e65a28eadb63519b5a5 Mon Sep 17 00:00:00 2001 From: d-walsh <2351963+d-walsh@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:34:39 -0400 Subject: [PATCH 3/3] review: add intentional-5-speeds comment; rename misleading tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add inline comment at the speeds == 3 branch explaining that 3-speed-reporting units under-report their capability and intentionally receive the full 5-speed list. - Rename test_set_fan_speed_accepts_superquiet_on_3speed → test_get_fan_speeds_includes_superquiet_on_3speed and test_set_fan_speed_accepts_superpowerful_on_3speed → test_get_fan_speeds_includes_superpowerful_on_3speed. These tests assert get_fan_speeds() membership, not set_fan_speed() behaviour; rename + docstring fix makes that accurate. Co-Authored-By: Claude --- pykumo/py_kumo.py | 4 ++++ tests/test_fan_speeds.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pykumo/py_kumo.py b/pykumo/py_kumo.py index 06040c9..48fb158 100644 --- a/pykumo/py_kumo.py +++ b/pykumo/py_kumo.py @@ -389,6 +389,10 @@ def get_fan_speeds(self): ) if speeds == 3: + # Intentionally return all 5 speeds even though the profile reports + # numberOfFanSpeeds=3. These units under-report their capability: + # real hardware accepts the full superQuiet..superPowerful range, + # as confirmed on units in the field. valid_speeds = ["superQuiet", "quiet", "low", "powerful", "superPowerful"] elif speeds == 4: valid_speeds = ["quiet", "Low", "powerful", "superPowerful"] diff --git a/tests/test_fan_speeds.py b/tests/test_fan_speeds.py index 7562def..ca05ca5 100644 --- a/tests/test_fan_speeds.py +++ b/tests/test_fan_speeds.py @@ -62,8 +62,8 @@ def test_5speed_unchanged(self): speeds, ["superQuiet", "quiet", "low", "powerful", "superPowerful"] ) - def test_set_fan_speed_accepts_superquiet_on_3speed(self): - """set_fan_speed must accept superQuiet for 3-speed units.""" + def test_get_fan_speeds_includes_superquiet_on_3speed(self): + """get_fan_speeds() must include superQuiet for 3-speed units.""" with patch.object(PyKumo, "__init__", lambda self, *a, **kw: None): unit = PyKumo.__new__(PyKumo) unit._profile = {"numberOfFanSpeeds": 3} @@ -71,8 +71,8 @@ def test_set_fan_speed_accepts_superquiet_on_3speed(self): valid = unit.get_fan_speeds() self.assertIn("superQuiet", valid) - def test_set_fan_speed_accepts_superpowerful_on_3speed(self): - """set_fan_speed must accept superPowerful for 3-speed units.""" + def test_get_fan_speeds_includes_superpowerful_on_3speed(self): + """get_fan_speeds() must include superPowerful for 3-speed units.""" with patch.object(PyKumo, "__init__", lambda self, *a, **kw: None): unit = PyKumo.__new__(PyKumo) unit._profile = {"numberOfFanSpeeds": 3}