Use backend-provided fan speed presets for Xiaomi vacuums, bum… (#32850)

* Use backend-provided fan speed presets for Xiaomi vacuums

This needs input from Xiaomi vacuum owners to verify that it does not break anything.
I have personally tested this on rockrobo v1 (old mapping).

Related issues/PRs:
home-assistant/core#32821
home-assistant/core#31268
home-assistant/core#27268

This is a WIP as it requires a new upstream release.
The PR is https://github.com/rytilahti/python-miio/pull/643

* Bump version requirement for 0.5.0

* Bump requirements_test_all.txt, too

* Fix linting; missing setup.cfg on local checkout caused wrong settings for black..

* Add tests for both fan speed types

* Remove useless else..

* bump python-miio to 0.5.0.1 due to broken 0.5.0 packaging
This commit is contained in:
Teemu R 2020-04-03 01:55:44 +02:00 committed by GitHub
parent cb058ff6c0
commit e64104300f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 110 additions and 60 deletions

View file

@ -2,7 +2,7 @@
"domain": "xiaomi_miio",
"name": "Xiaomi miio",
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
"requirements": ["construct==2.9.45", "python-miio==0.4.8"],
"requirements": ["construct==2.9.45", "python-miio==0.5.0.1"],
"dependencies": [],
"codeowners": ["@rytilahti", "@syssi"]
}

View file

@ -60,8 +60,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
extra=vol.ALLOW_EXTRA,
)
FAN_SPEEDS = {"Silent": 38, "Standard": 60, "Medium": 77, "Turbo": 90, "Gentle": 105}
ATTR_CLEAN_START = "clean_start"
ATTR_CLEAN_STOP = "clean_stop"
ATTR_CLEANING_TIME = "cleaning_time"
@ -246,6 +244,8 @@ class MiroboVacuum(StateVacuumDevice):
self.clean_history = None
self.dnd_state = None
self.last_clean = None
self._fan_speeds = None
self._fan_speeds_reverse = None
@property
def name(self):
@ -281,14 +281,17 @@ class MiroboVacuum(StateVacuumDevice):
"""Return the fan speed of the vacuum cleaner."""
if self.vacuum_state is not None:
speed = self.vacuum_state.fanspeed
if speed in FAN_SPEEDS.values():
return [key for key, value in FAN_SPEEDS.items() if value == speed][0]
if speed in self._fan_speeds_reverse:
return self._fan_speeds_reverse[speed]
_LOGGER.debug("Unable to find reverse for %s", speed)
return speed
@property
def fan_speed_list(self):
"""Get the list of available fan speed steps of the vacuum cleaner."""
return list(sorted(FAN_SPEEDS.keys(), key=lambda s: FAN_SPEEDS[s]))
return list(self._fan_speeds)
@property
def device_state_attributes(self):
@ -372,8 +375,8 @@ class MiroboVacuum(StateVacuumDevice):
async def async_set_fan_speed(self, fan_speed, **kwargs):
"""Set fan speed."""
if fan_speed.capitalize() in FAN_SPEEDS:
fan_speed = FAN_SPEEDS[fan_speed.capitalize()]
if fan_speed in self._fan_speeds:
fan_speed = self._fan_speeds[fan_speed]
else:
try:
fan_speed = int(fan_speed)
@ -453,6 +456,9 @@ class MiroboVacuum(StateVacuumDevice):
state = self._vacuum.status()
self.vacuum_state = state
self._fan_speeds = self._vacuum.fan_speed_presets()
self._fan_speeds_reverse = {v: k for k, v in self._fan_speeds.items()}
self.consumable_state = self._vacuum.consumable_status()
self.clean_history = self._vacuum.clean_history()
self.last_clean = self._vacuum.last_clean_details()

View file

@ -1638,7 +1638,7 @@ python-juicenet==0.1.6
# python-lirc==1.2.3
# homeassistant.components.xiaomi_miio
python-miio==0.4.8
python-miio==0.5.0.1
# homeassistant.components.mpd
python-mpd2==1.0.0

View file

@ -620,7 +620,7 @@ python-forecastio==1.4.0
python-izone==1.1.2
# homeassistant.components.xiaomi_miio
python-miio==0.4.8
python-miio==0.5.0.1
# homeassistant.components.nest
python-nest==4.1.0

View file

@ -100,6 +100,36 @@ def mirobo_is_got_error_fixture():
yield mock_vacuum
old_fanspeeds = {
"Silent": 38,
"Standard": 60,
"Medium": 77,
"Turbo": 90,
}
new_fanspeeds = {
"Silent": 101,
"Standard": 102,
"Medium": 103,
"Turbo": 104,
"Gentle": 105,
}
@pytest.fixture(name="mock_mirobo_fanspeeds", params=[old_fanspeeds, new_fanspeeds])
def mirobo_old_speeds_fixture(request):
"""Fixture for testing both types of fanspeeds."""
mock_vacuum = mock.MagicMock()
mock_vacuum.status().battery = 32
mock_vacuum.fan_speed_presets.return_value = request.param
mock_vacuum.status().fanspeed = list(request.param.values())[0]
with mock.patch(
"homeassistant.components.xiaomi_miio.vacuum.Vacuum"
) as mock_vaccum_cls:
mock_vaccum_cls.return_value = mock_vacuum
yield mock_vacuum
@pytest.fixture(name="mock_mirobo_is_on")
def mirobo_is_on_fixture():
"""Mock mock_mirobo."""
@ -204,14 +234,6 @@ async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error):
assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-80"
assert state.attributes.get(ATTR_CLEANING_TIME) == 155
assert state.attributes.get(ATTR_CLEANED_AREA) == 123
assert state.attributes.get(ATTR_FAN_SPEED) == "Silent"
assert state.attributes.get(ATTR_FAN_SPEED_LIST) == [
"Silent",
"Standard",
"Medium",
"Turbo",
"Gentle",
]
assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 12
assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 12
assert state.attributes.get(ATTR_FILTER_LEFT) == 12
@ -257,40 +279,6 @@ async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error):
mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True)
mock_mirobo_is_got_error.reset_mock()
# Set speed service:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_FAN_SPEED,
{"entity_id": entity_id, "fan_speed": 60},
blocking=True,
)
mock_mirobo_is_got_error.assert_has_calls(
[mock.call.set_fan_speed(60)], any_order=True
)
mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True)
mock_mirobo_is_got_error.reset_mock()
await hass.services.async_call(
DOMAIN,
SERVICE_SET_FAN_SPEED,
{"entity_id": entity_id, "fan_speed": "Medium"},
blocking=True,
)
mock_mirobo_is_got_error.assert_has_calls(
[mock.call.set_fan_speed(77)], any_order=True
)
mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True)
mock_mirobo_is_got_error.reset_mock()
assert "ERROR" not in caplog.text
await hass.services.async_call(
DOMAIN,
SERVICE_SET_FAN_SPEED,
{"entity_id": entity_id, "fan_speed": "invent"},
blocking=True,
)
assert "ERROR" in caplog.text
await hass.services.async_call(
DOMAIN,
SERVICE_SEND_COMMAND,
@ -346,14 +334,6 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on):
assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-30"
assert state.attributes.get(ATTR_CLEANING_TIME) == 175
assert state.attributes.get(ATTR_CLEANED_AREA) == 133
assert state.attributes.get(ATTR_FAN_SPEED) == 99
assert state.attributes.get(ATTR_FAN_SPEED_LIST) == [
"Silent",
"Standard",
"Medium",
"Turbo",
"Gentle",
]
assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 11
assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 11
assert state.attributes.get(ATTR_FILTER_LEFT) == 11
@ -409,3 +389,67 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on):
)
mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True)
mock_mirobo_is_on.reset_mock()
async def test_xiaomi_vacuum_fanspeeds(hass, caplog, mock_mirobo_fanspeeds):
"""Test Xiaomi vacuum fanspeeds."""
entity_name = "test_vacuum_cleaner_2"
entity_id = f"{DOMAIN}.{entity_name}"
await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: {
CONF_PLATFORM: PLATFORM,
CONF_HOST: "192.168.1.100",
CONF_NAME: entity_name,
CONF_TOKEN: "12345678901234567890123456789012",
}
},
)
await hass.async_block_till_done()
assert "Initializing with host 192.168.1.100 (token 12345" in caplog.text
state = hass.states.get(entity_id)
assert state.attributes.get(ATTR_FAN_SPEED) == "Silent"
fanspeeds = state.attributes.get(ATTR_FAN_SPEED_LIST)
for speed in ["Silent", "Standard", "Medium", "Turbo"]:
assert speed in fanspeeds
# Set speed service:
await hass.services.async_call(
DOMAIN,
SERVICE_SET_FAN_SPEED,
{"entity_id": entity_id, "fan_speed": 60},
blocking=True,
)
mock_mirobo_fanspeeds.assert_has_calls(
[mock.call.set_fan_speed(60)], any_order=True
)
mock_mirobo_fanspeeds.assert_has_calls(STATUS_CALLS, any_order=True)
mock_mirobo_fanspeeds.reset_mock()
fan_speed_dict = mock_mirobo_fanspeeds.fan_speed_presets()
await hass.services.async_call(
DOMAIN,
SERVICE_SET_FAN_SPEED,
{"entity_id": entity_id, "fan_speed": "Medium"},
blocking=True,
)
mock_mirobo_fanspeeds.assert_has_calls(
[mock.call.set_fan_speed(fan_speed_dict["Medium"])], any_order=True
)
mock_mirobo_fanspeeds.assert_has_calls(STATUS_CALLS, any_order=True)
mock_mirobo_fanspeeds.reset_mock()
assert "ERROR" not in caplog.text
await hass.services.async_call(
DOMAIN,
SERVICE_SET_FAN_SPEED,
{"entity_id": entity_id, "fan_speed": "invent"},
blocking=True,
)
assert "ERROR" in caplog.text