Add additional select for dmaker.airfresh.t2017 to xiaomi_miio (#67058)

This commit is contained in:
Igor Pakhomov 2022-08-16 18:30:56 +03:00 committed by GitHub
parent d50b5cebee
commit 65f86ce44f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 329 additions and 160 deletions

View file

@ -1478,7 +1478,6 @@ omit =
homeassistant/components/xiaomi_miio/light.py
homeassistant/components/xiaomi_miio/number.py
homeassistant/components/xiaomi_miio/remote.py
homeassistant/components/xiaomi_miio/select.py
homeassistant/components/xiaomi_miio/sensor.py
homeassistant/components/xiaomi_miio/switch.py
homeassistant/components/xiaomi_tv/media_player.py

View file

@ -1,9 +1,14 @@
"""Support led_brightness for Mi Air Humidifier."""
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import NamedTuple
from miio.fan_common import LedBrightness as FanLedBrightness
from miio.integrations.airpurifier.dmaker.airfresh_t2017 import (
DisplayOrientation as AirfreshT2017DisplayOrientation,
PtcLevel as AirfreshT2017PtcLevel,
)
from miio.integrations.airpurifier.zhimi.airfresh import (
LedBrightness as AirfreshLedBrightness,
)
@ -31,53 +36,134 @@ from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
DOMAIN,
FEATURE_SET_LED_BRIGHTNESS,
KEY_COORDINATOR,
KEY_DEVICE,
MODEL_AIRFRESH_T2017,
MODEL_AIRFRESH_VA2,
MODEL_AIRPURIFIER_3C,
MODEL_AIRHUMIDIFIER_CA1,
MODEL_AIRHUMIDIFIER_CA4,
MODEL_AIRHUMIDIFIER_CB1,
MODEL_AIRHUMIDIFIER_V1,
MODEL_AIRPURIFIER_3,
MODEL_AIRPURIFIER_3H,
MODEL_AIRPURIFIER_M1,
MODEL_AIRPURIFIER_M2,
MODEL_AIRPURIFIER_PROH,
MODEL_FAN_SA1,
MODEL_FAN_V2,
MODEL_FAN_V3,
MODEL_FAN_ZA1,
MODEL_FAN_ZA3,
MODEL_FAN_ZA4,
MODELS_HUMIDIFIER_MIIO,
MODELS_HUMIDIFIER_MIOT,
MODELS_PURIFIER_MIOT,
)
from .device import XiaomiCoordinatedMiioEntity
ATTR_DISPLAY_ORIENTATION = "display_orientation"
ATTR_LED_BRIGHTNESS = "led_brightness"
LED_BRIGHTNESS_MAP = {"Bright": 0, "Dim": 1, "Off": 2}
LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT = {"Bright": 2, "Dim": 1, "Off": 0}
LED_BRIGHTNESS_REVERSE_MAP = {val: key for key, val in LED_BRIGHTNESS_MAP.items()}
LED_BRIGHTNESS_REVERSE_MAP_HUMIDIFIER_MIOT = {
val: key for key, val in LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT.items()
}
ATTR_PTC_LEVEL = "ptc_level"
@dataclass
class XiaomiMiioSelectDescription(SelectEntityDescription):
"""A class that describes select entities."""
attr_name: str = ""
options_map: dict = field(default_factory=dict)
set_method: str = ""
set_method_error_message: str = ""
options: tuple = ()
SELECTOR_TYPES = {
FEATURE_SET_LED_BRIGHTNESS: XiaomiMiioSelectDescription(
class AttributeEnumMapping(NamedTuple):
"""Class to mapping Attribute to Enum Class."""
attr_name: str
enum_class: type
MODEL_TO_ATTR_MAP: dict[str, list] = {
MODEL_AIRFRESH_T2017: [
AttributeEnumMapping(ATTR_DISPLAY_ORIENTATION, AirfreshT2017DisplayOrientation),
AttributeEnumMapping(ATTR_PTC_LEVEL, AirfreshT2017PtcLevel),
],
MODEL_AIRFRESH_VA2: [
AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirfreshLedBrightness)
],
MODEL_AIRHUMIDIFIER_CA1: [
AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierLedBrightness)
],
MODEL_AIRHUMIDIFIER_CA4: [
AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierMiotLedBrightness)
],
MODEL_AIRHUMIDIFIER_CB1: [
AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierLedBrightness)
],
MODEL_AIRHUMIDIFIER_V1: [
AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierLedBrightness)
],
MODEL_AIRPURIFIER_3: [
AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness)
],
MODEL_AIRPURIFIER_3H: [
AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness)
],
MODEL_AIRPURIFIER_M1: [
AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierLedBrightness)
],
MODEL_AIRPURIFIER_M2: [
AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierLedBrightness)
],
MODEL_AIRPURIFIER_PROH: [
AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness)
],
MODEL_FAN_SA1: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)],
MODEL_FAN_V2: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)],
MODEL_FAN_V3: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)],
MODEL_FAN_ZA1: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)],
MODEL_FAN_ZA3: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)],
MODEL_FAN_ZA4: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)],
}
SELECTOR_TYPES = (
XiaomiMiioSelectDescription(
key=ATTR_DISPLAY_ORIENTATION,
attr_name=ATTR_DISPLAY_ORIENTATION,
name="Display Orientation",
options_map={
"Portrait": "Forward",
"LandscapeLeft": "Left",
"LandscapeRight": "Right",
},
set_method="set_display_orientation",
set_method_error_message="Setting the display orientation failed.",
icon="mdi:tablet",
device_class="xiaomi_miio__display_orientation",
options=("forward", "left", "right"),
entity_category=EntityCategory.CONFIG,
),
XiaomiMiioSelectDescription(
key=ATTR_LED_BRIGHTNESS,
attr_name=ATTR_LED_BRIGHTNESS,
name="Led Brightness",
set_method="set_led_brightness",
set_method_error_message="Setting the led brightness failed.",
icon="mdi:brightness-6",
device_class="xiaomi_miio__led_brightness",
options=("bright", "dim", "off"),
entity_category=EntityCategory.CONFIG,
),
}
XiaomiMiioSelectDescription(
key=ATTR_PTC_LEVEL,
attr_name=ATTR_PTC_LEVEL,
name="Auxiliary Heat Level",
set_method="set_ptc_level",
set_method_error_message="Setting the ptc level failed.",
icon="mdi:fire-circle",
device_class="xiaomi_miio__ptc_level",
options=("low", "medium", "high"),
entity_category=EntityCategory.CONFIG,
),
)
async def async_setup_entry(
@ -89,44 +175,29 @@ async def async_setup_entry(
if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
return
entities = []
model = config_entry.data[CONF_MODEL]
if model not in MODEL_TO_ATTR_MAP:
return
entities = []
unique_id = config_entry.unique_id
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
attributes = MODEL_TO_ATTR_MAP[model]
if model == MODEL_AIRPURIFIER_3C:
return
if model in MODELS_HUMIDIFIER_MIIO:
entity_class = XiaomiAirHumidifierSelector
elif model in MODELS_HUMIDIFIER_MIOT:
entity_class = XiaomiAirHumidifierMiotSelector
elif model in [MODEL_AIRPURIFIER_M1, MODEL_AIRPURIFIER_M2]:
entity_class = XiaomiAirPurifierSelector
elif model in MODELS_PURIFIER_MIOT:
entity_class = XiaomiAirPurifierMiotSelector
elif model == MODEL_AIRFRESH_VA2:
entity_class = XiaomiAirFreshSelector
elif model in (
MODEL_FAN_ZA1,
MODEL_FAN_ZA3,
MODEL_FAN_ZA4,
MODEL_FAN_SA1,
MODEL_FAN_V2,
MODEL_FAN_V3,
):
entity_class = XiaomiFanSelector
else:
return
description = SELECTOR_TYPES[FEATURE_SET_LED_BRIGHTNESS]
entities.append(
entity_class(
device,
config_entry,
f"{description.key}_{config_entry.unique_id}",
hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR],
description,
)
)
for description in SELECTOR_TYPES:
for attribute in attributes:
if description.key == attribute.attr_name:
entities.append(
XiaomiGenericSelector(
device,
config_entry,
f"{description.key}_{unique_id}",
coordinator,
description,
attribute.enum_class,
)
)
async_add_entities(entities)
@ -141,129 +212,60 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity):
self.entity_description = description
class XiaomiAirHumidifierSelector(XiaomiSelector):
"""Representation of a Xiaomi Air Humidifier selector."""
class XiaomiGenericSelector(XiaomiSelector):
"""Representation of a Xiaomi generic selector."""
def __init__(self, device, entry, unique_id, coordinator, description):
"""Initialize the plug switch."""
entity_description: XiaomiMiioSelectDescription
def __init__(self, device, entry, unique_id, coordinator, description, enum_class):
"""Initialize the generic Xiaomi attribute selector."""
super().__init__(device, entry, unique_id, coordinator, description)
self._current_led_brightness = self._extract_value_from_attribute(
self.coordinator.data, self.entity_description.key
self._current_attr = enum_class(
self._extract_value_from_attribute(
self.coordinator.data, self.entity_description.attr_name
)
)
if description.options_map:
self._options_map = {}
for key, val in enum_class._member_map_.items():
self._options_map[description.options_map[key]] = val
else:
self._options_map = enum_class._member_map_
self._reverse_map = {val: key for key, val in self._options_map.items()}
self._enum_class = enum_class
@callback
def _handle_coordinator_update(self):
"""Fetch state from the device."""
led_brightness = self._extract_value_from_attribute(
self.coordinator.data, self.entity_description.key
attr = self._enum_class(
self._extract_value_from_attribute(
self.coordinator.data, self.entity_description.attr_name
)
)
# Sometimes (quite rarely) the device returns None as the LED brightness so we
# check that the value is not None before updating the state.
if led_brightness is not None:
self._current_led_brightness = led_brightness
if attr is not None:
self._current_attr = attr
self.async_write_ha_state()
@property
def current_option(self) -> str:
def current_option(self) -> str | None:
"""Return the current option."""
return self.led_brightness.lower()
option = self._reverse_map.get(self._current_attr)
if option is not None:
return option.lower()
return None
async def async_select_option(self, option: str) -> None:
"""Set an option of the miio device."""
await self.async_set_led_brightness(option.title())
await self.async_set_attr(option.title())
@property
def led_brightness(self):
"""Return the current led brightness."""
return LED_BRIGHTNESS_REVERSE_MAP.get(self._current_led_brightness)
async def async_set_led_brightness(self, brightness: str):
"""Set the led brightness."""
async def async_set_attr(self, attr_value: str):
"""Set attr."""
method = getattr(self._device, self.entity_description.set_method)
if await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness,
AirhumidifierLedBrightness(LED_BRIGHTNESS_MAP[brightness]),
self.entity_description.set_method_error_message,
method,
self._enum_class(self._options_map[attr_value]),
):
self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness]
self.async_write_ha_state()
class XiaomiAirHumidifierMiotSelector(XiaomiAirHumidifierSelector):
"""Representation of a Xiaomi Air Humidifier (MiOT protocol) selector."""
@property
def led_brightness(self):
"""Return the current led brightness."""
return LED_BRIGHTNESS_REVERSE_MAP_HUMIDIFIER_MIOT.get(
self._current_led_brightness
)
async def async_set_led_brightness(self, brightness: str) -> None:
"""Set the led brightness."""
if await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness,
AirhumidifierMiotLedBrightness(
LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT[brightness]
),
):
self._current_led_brightness = LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT[
brightness
]
self.async_write_ha_state()
class XiaomiAirPurifierSelector(XiaomiAirHumidifierSelector):
"""Representation of a Xiaomi Air Purifier (MIIO protocol) selector."""
async def async_set_led_brightness(self, brightness: str) -> None:
"""Set the led brightness."""
if await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness,
AirpurifierLedBrightness(LED_BRIGHTNESS_MAP[brightness]),
):
self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness]
self.async_write_ha_state()
class XiaomiAirPurifierMiotSelector(XiaomiAirHumidifierSelector):
"""Representation of a Xiaomi Air Purifier (MiOT protocol) selector."""
async def async_set_led_brightness(self, brightness: str) -> None:
"""Set the led brightness."""
if await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness,
AirpurifierMiotLedBrightness(LED_BRIGHTNESS_MAP[brightness]),
):
self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness]
self.async_write_ha_state()
class XiaomiFanSelector(XiaomiAirHumidifierSelector):
"""Representation of a Xiaomi Fan (MIIO protocol) selector."""
async def async_set_led_brightness(self, brightness: str) -> None:
"""Set the led brightness."""
if await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness,
FanLedBrightness(LED_BRIGHTNESS_MAP[brightness]),
):
self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness]
self.async_write_ha_state()
class XiaomiAirFreshSelector(XiaomiAirHumidifierSelector):
"""Representation of a Xiaomi Air Fresh selector."""
async def async_set_led_brightness(self, brightness: str) -> None:
"""Set the led brightness."""
if await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness,
AirfreshLedBrightness(LED_BRIGHTNESS_MAP[brightness]),
):
self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness]
self._current_attr = self._options_map[attr_value]
self.async_write_ha_state()

View file

@ -4,6 +4,16 @@
"bright": "Bright",
"dim": "Dim",
"off": "Off"
},
"xiaomi_miio__display_orientation": {
"forward": "Forward",
"left": "Left",
"right": "Right"
},
"xiaomi_miio__ptc_level": {
"low": "Low",
"medium": "Medium",
"high": "High"
}
}
}

View file

@ -0,0 +1,158 @@
"""The tests for the xiaomi_miio select component."""
from unittest.mock import MagicMock, patch
from arrow import utcnow
from miio.integrations.airpurifier.dmaker.airfresh_t2017 import (
DisplayOrientation,
PtcLevel,
)
import pytest
from homeassistant.components.select import DOMAIN
from homeassistant.components.select.const import (
ATTR_OPTION,
ATTR_OPTIONS,
SERVICE_SELECT_OPTION,
)
from homeassistant.components.xiaomi_miio import UPDATE_INTERVAL
from homeassistant.components.xiaomi_miio.const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_MAC,
DOMAIN as XIAOMI_DOMAIN,
MODEL_AIRFRESH_T2017,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
CONF_MODEL,
CONF_TOKEN,
Platform,
)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.xiaomi_miio import TEST_MAC
@pytest.fixture(autouse=True)
async def setup_test(hass: HomeAssistant):
"""Initialize test xiaomi_miio for select entity."""
mock_airfresh = MagicMock()
mock_airfresh.status().display_orientation = DisplayOrientation.Portrait
mock_airfresh.status().ptc_level = PtcLevel.Low
with patch(
"homeassistant.components.xiaomi_miio.get_platforms",
return_value=[
Platform.SELECT,
],
), patch("homeassistant.components.xiaomi_miio.AirFreshT2017") as mock_airfresh_cls:
mock_airfresh_cls.return_value = mock_airfresh
yield mock_airfresh
async def test_select_params(hass: HomeAssistant) -> None:
"""Test the initial parameters."""
entity_name = "test_airfresh_select"
entity_id = await setup_component(hass, entity_name)
select_entity = hass.states.get(entity_id + "_display_orientation")
assert select_entity
assert select_entity.state == "forward"
assert select_entity.attributes.get(ATTR_OPTIONS) == ["forward", "left", "right"]
async def test_select_bad_attr(hass: HomeAssistant) -> None:
"""Test selecting a different option with invalid option value."""
entity_name = "test_airfresh_select"
entity_id = await setup_component(hass, entity_name)
state = hass.states.get(entity_id + "_display_orientation")
assert state
assert state.state == "forward"
with pytest.raises(ValueError):
await hass.services.async_call(
"select",
SERVICE_SELECT_OPTION,
{ATTR_OPTION: "up", ATTR_ENTITY_ID: entity_id + "_display_orientation"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(entity_id + "_display_orientation")
assert state
assert state.state == "forward"
async def test_select_option(hass: HomeAssistant) -> None:
"""Test selecting of a option."""
entity_name = "test_airfresh_select"
entity_id = await setup_component(hass, entity_name)
state = hass.states.get(entity_id + "_display_orientation")
assert state
assert state.state == "forward"
await hass.services.async_call(
"select",
SERVICE_SELECT_OPTION,
{ATTR_OPTION: "left", ATTR_ENTITY_ID: entity_id + "_display_orientation"},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(entity_id + "_display_orientation")
assert state
assert state.state == "left"
async def test_select_coordinator_update(hass: HomeAssistant, setup_test) -> None:
"""Test coordinator update of a option."""
entity_name = "test_airfresh_select"
entity_id = await setup_component(hass, entity_name)
state = hass.states.get(entity_id + "_display_orientation")
assert state
assert state.state == "forward"
# emulate someone change state from device maybe used app
setup_test.status().display_orientation = DisplayOrientation.LandscapeLeft
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL)
await hass.async_block_till_done()
state = hass.states.get(entity_id + "_display_orientation")
assert state
assert state.state == "left"
async def setup_component(hass, entity_name):
"""Set up component."""
entity_id = f"{DOMAIN}.{entity_name}"
config_entry = MockConfigEntry(
domain=XIAOMI_DOMAIN,
unique_id="123456",
title=entity_name,
data={
CONF_FLOW_TYPE: CONF_DEVICE,
CONF_HOST: "0.0.0.0",
CONF_TOKEN: "12345678901234567890123456789012",
CONF_MODEL: MODEL_AIRFRESH_T2017,
CONF_MAC: TEST_MAC,
},
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return entity_id