938 lines
33 KiB
Python
938 lines
33 KiB
Python
"""Midea Climate entries."""
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any, ClassVar, cast
|
|
|
|
from homeassistant.components.climate import (
|
|
ATTR_HVAC_MODE,
|
|
FAN_AUTO,
|
|
FAN_HIGH,
|
|
FAN_LOW,
|
|
FAN_MEDIUM,
|
|
PRESET_AWAY,
|
|
PRESET_BOOST,
|
|
PRESET_COMFORT,
|
|
PRESET_ECO,
|
|
PRESET_NONE,
|
|
PRESET_SLEEP,
|
|
SWING_BOTH,
|
|
SWING_HORIZONTAL,
|
|
SWING_OFF,
|
|
SWING_ON,
|
|
SWING_VERTICAL,
|
|
ClimateEntity,
|
|
ClimateEntityFeature,
|
|
HVACMode,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import (
|
|
ATTR_TEMPERATURE,
|
|
CONF_CUSTOMIZE,
|
|
CONF_DEVICE_ID,
|
|
CONF_SWITCHES,
|
|
MAJOR_VERSION,
|
|
MINOR_VERSION,
|
|
PRECISION_HALVES,
|
|
PRECISION_WHOLE,
|
|
Platform,
|
|
UnitOfTemperature,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from midealocal.device import DeviceType
|
|
from midealocal.devices.ac import DeviceAttributes as ACAttributes
|
|
from midealocal.devices.ac import MideaACDevice
|
|
from midealocal.devices.c3 import DeviceAttributes as C3Attributes
|
|
from midealocal.devices.c3 import MideaC3Device
|
|
from midealocal.devices.cc import DeviceAttributes as CCAttributes
|
|
from midealocal.devices.cc import MideaCCDevice
|
|
from midealocal.devices.cf import DeviceAttributes as CFAttributes
|
|
from midealocal.devices.cf import MideaCFDevice
|
|
from midealocal.devices.fb import DeviceAttributes as FBAttributes
|
|
from midealocal.devices.fb import MideaFBDevice
|
|
|
|
from .const import DEVICES, DOMAIN, FanSpeed
|
|
from .midea_devices import MIDEA_DEVICES
|
|
from .midea_entity import MideaEntity
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
TEMPERATURE_MAX = 30
|
|
TEMPERATURE_MIN = 16
|
|
|
|
FAN_SILENT = "silent"
|
|
FAN_FULL_SPEED = "full"
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up climate entries."""
|
|
device_id = config_entry.data.get(CONF_DEVICE_ID)
|
|
device = hass.data[DOMAIN][DEVICES].get(device_id)
|
|
extra_switches = config_entry.options.get(CONF_SWITCHES, [])
|
|
devs: list[
|
|
MideaACClimate
|
|
| MideaCCClimate
|
|
| MideaCFClimate
|
|
| MideaC3Climate
|
|
| MideaFBClimate
|
|
] = []
|
|
for entity_key, config in cast(
|
|
"dict",
|
|
MIDEA_DEVICES[device.device_type]["entities"],
|
|
).items():
|
|
if config["type"] == Platform.CLIMATE and (
|
|
config.get("default") or entity_key in extra_switches
|
|
):
|
|
if device.device_type == DeviceType.AC:
|
|
# add config_entry args to fix indoor_humidity error bug
|
|
devs.append(MideaACClimate(device, entity_key, config_entry))
|
|
elif device.device_type == DeviceType.CC:
|
|
devs.append(MideaCCClimate(device, entity_key))
|
|
elif device.device_type == DeviceType.CF:
|
|
devs.append(MideaCFClimate(device, entity_key))
|
|
elif device.device_type == DeviceType.C3:
|
|
devs.append(MideaC3Climate(device, entity_key, config["zone"]))
|
|
elif device.device_type == DeviceType.FB:
|
|
devs.append(MideaFBClimate(device, entity_key))
|
|
async_add_entities(devs)
|
|
|
|
|
|
type MideaClimateDevice = (
|
|
MideaACDevice | MideaCCDevice | MideaCFDevice | MideaC3Device | MideaFBDevice
|
|
)
|
|
|
|
|
|
class MideaClimate(MideaEntity, ClimateEntity):
|
|
"""Midea Climate Entries Base Class."""
|
|
|
|
# https://developers.home-assistant.io/blog/2024/01/24/climate-climateentityfeatures-expanded
|
|
_enable_turn_on_off_backwards_compatibility: bool = (
|
|
False # maybe remove after 2025.1
|
|
)
|
|
|
|
_device: MideaClimateDevice
|
|
|
|
_attr_max_temp: float = TEMPERATURE_MAX
|
|
_attr_min_temp: float = TEMPERATURE_MIN
|
|
_attr_target_temperature_high: float | None = TEMPERATURE_MAX
|
|
_attr_target_temperature_low: float | None = TEMPERATURE_MIN
|
|
_attr_temperature_unit: str = UnitOfTemperature.CELSIUS
|
|
|
|
def __init__(self, device: MideaClimateDevice, entity_key: str) -> None:
|
|
"""Midea Climate entity init."""
|
|
super().__init__(device, entity_key)
|
|
|
|
@property
|
|
def supported_features(self) -> ClimateEntityFeature:
|
|
"""Midea Climate supported features."""
|
|
features = (
|
|
ClimateEntityFeature.TARGET_TEMPERATURE
|
|
| ClimateEntityFeature.FAN_MODE
|
|
| ClimateEntityFeature.PRESET_MODE
|
|
| ClimateEntityFeature.SWING_MODE
|
|
)
|
|
if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 2):
|
|
features |= ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
|
return features
|
|
|
|
@property
|
|
def hvac_mode(self) -> HVACMode:
|
|
"""Midea Climate hvac mode."""
|
|
if self._device.get_attribute("power"):
|
|
mode = cast("int", self._device.get_attribute("mode"))
|
|
return self.hvac_modes[mode]
|
|
return HVACMode.OFF
|
|
|
|
@property
|
|
def target_temperature(self) -> float:
|
|
"""Midea Climate target temperature."""
|
|
return cast("float", self._device.get_attribute("target_temperature"))
|
|
|
|
@property
|
|
def current_temperature(self) -> float | None:
|
|
"""Midea Climate current temperature."""
|
|
return cast("float | None", self._device.get_attribute("indoor_temperature"))
|
|
|
|
@property
|
|
def preset_mode(self) -> str:
|
|
"""Midea Climate preset mode."""
|
|
if self._device.get_attribute("comfort_mode"):
|
|
mode = PRESET_COMFORT
|
|
elif self._device.get_attribute("eco_mode"):
|
|
mode = PRESET_ECO
|
|
elif self._device.get_attribute("boost_mode"):
|
|
mode = PRESET_BOOST
|
|
elif self._device.get_attribute("sleep_mode"):
|
|
mode = PRESET_SLEEP
|
|
elif self._device.get_attribute("frost_protect"):
|
|
mode = PRESET_AWAY
|
|
else:
|
|
mode = PRESET_NONE
|
|
return str(mode)
|
|
|
|
@property
|
|
def extra_state_attributes(self) -> dict:
|
|
"""Midea Climate extra state attributes."""
|
|
return cast("dict", self._device.attributes)
|
|
|
|
def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
|
|
"""Midea Climate turn on."""
|
|
self._device.set_attribute(attr="power", value=True)
|
|
|
|
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
|
|
"""Midea Climate turn off."""
|
|
self._device.set_attribute(attr="power", value=False)
|
|
|
|
def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
|
|
"""Midea Climate set temperature."""
|
|
if ATTR_TEMPERATURE not in kwargs:
|
|
return
|
|
temperature = float(int((float(kwargs[ATTR_TEMPERATURE]) * 2) + 0.5)) / 2
|
|
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
|
if hvac_mode == HVACMode.OFF:
|
|
self.turn_off()
|
|
else:
|
|
try:
|
|
mode = self.hvac_modes.index(hvac_mode.lower()) if hvac_mode else None
|
|
self._device.set_target_temperature(
|
|
target_temperature=temperature,
|
|
mode=mode,
|
|
zone=None,
|
|
)
|
|
except ValueError:
|
|
_LOGGER.exception("Error setting temperature with: %s", kwargs)
|
|
|
|
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
"""Midea Climate set hvac mode."""
|
|
if hvac_mode == HVACMode.OFF:
|
|
self.turn_off()
|
|
else:
|
|
self._device.set_attribute(
|
|
attr="mode",
|
|
value=self.hvac_modes.index(hvac_mode),
|
|
)
|
|
|
|
def set_preset_mode(self, preset_mode: str) -> None:
|
|
"""Midea Climate set preset mode."""
|
|
old_mode = self.preset_mode
|
|
preset_mode = preset_mode.lower()
|
|
if preset_mode == PRESET_AWAY:
|
|
self._device.set_attribute(attr="frost_protect", value=True)
|
|
elif preset_mode == PRESET_COMFORT:
|
|
self._device.set_attribute(attr="comfort_mode", value=True)
|
|
elif preset_mode == PRESET_SLEEP:
|
|
self._device.set_attribute(attr="sleep_mode", value=True)
|
|
elif preset_mode == PRESET_ECO:
|
|
self._device.set_attribute(attr="eco_mode", value=True)
|
|
elif preset_mode == PRESET_BOOST:
|
|
self._device.set_attribute(attr="boost_mode", value=True)
|
|
elif old_mode == PRESET_AWAY:
|
|
self._device.set_attribute(attr="frost_protect", value=False)
|
|
elif old_mode == PRESET_COMFORT:
|
|
self._device.set_attribute(attr="comfort_mode", value=False)
|
|
elif old_mode == PRESET_SLEEP:
|
|
self._device.set_attribute(attr="sleep_mode", value=False)
|
|
elif old_mode == PRESET_ECO:
|
|
self._device.set_attribute(attr="eco_mode", value=False)
|
|
elif old_mode == PRESET_BOOST:
|
|
self._device.set_attribute(attr="boost_mode", value=False)
|
|
|
|
def update_state(self, status: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
|
|
"""Midea Climate update state."""
|
|
if not self.hass:
|
|
_LOGGER.warning(
|
|
"Climate update_state skipped for %s [%s]: HASS is None",
|
|
self.name,
|
|
type(self),
|
|
)
|
|
return
|
|
self.schedule_update_if_running()
|
|
|
|
|
|
class MideaACClimate(MideaClimate):
|
|
"""Midea AC Climate Entries."""
|
|
|
|
_device: MideaACDevice
|
|
|
|
def __init__(
|
|
self,
|
|
device: MideaACDevice,
|
|
entity_key: str,
|
|
config_entry: ConfigEntry,
|
|
) -> None:
|
|
"""Midea AC Climate entity init."""
|
|
super().__init__(device, entity_key)
|
|
# fixed positional map: device "mode" int -> HVACMode. DO NOT reorder,
|
|
# the device mode value indexes into this list.
|
|
self._mode_index = [
|
|
HVACMode.OFF,
|
|
HVACMode.AUTO,
|
|
HVACMode.COOL,
|
|
HVACMode.DRY,
|
|
HVACMode.HEAT,
|
|
HVACMode.FAN_ONLY,
|
|
]
|
|
self._attr_hvac_modes = list(self._mode_index)
|
|
# customize overrides parsed once; B5 capabilities read dynamically
|
|
# (they arrive after the first refresh, so hvac_modes/supported_features
|
|
# are computed lazily — priority: customize > B5 > defaults)
|
|
self._customize_swing: bool | None = None
|
|
self._customize_hvac_modes: list[HVACMode] | None = None
|
|
self._customize_preset_modes: list[str] | None = None
|
|
self._parse_capability_customize(config_entry)
|
|
self._fan_speeds: dict[str, int] = {
|
|
FAN_SILENT: 20,
|
|
FAN_LOW: 40,
|
|
FAN_MEDIUM: 60,
|
|
FAN_HIGH: 80,
|
|
FAN_FULL_SPEED: 100,
|
|
FAN_AUTO: 102,
|
|
}
|
|
self._attr_swing_modes: list[str] = [
|
|
SWING_OFF,
|
|
SWING_VERTICAL,
|
|
SWING_HORIZONTAL,
|
|
SWING_BOTH,
|
|
]
|
|
self._attr_preset_modes = [
|
|
PRESET_NONE,
|
|
PRESET_COMFORT,
|
|
PRESET_ECO,
|
|
PRESET_BOOST,
|
|
PRESET_SLEEP,
|
|
PRESET_AWAY,
|
|
]
|
|
self._attr_fan_modes = list(self._fan_speeds.keys())
|
|
# fix error humidity value, disable indoor_humidity
|
|
# add config_entry args to fix indoor_humidity error bug
|
|
self._indoor_humidity_enabled = (
|
|
"sensors" in config_entry.options
|
|
and "indoor_humidity" in config_entry.options["sensors"]
|
|
)
|
|
|
|
def _parse_capability_customize(self, config_entry: ConfigEntry) -> None:
|
|
"""Parse the swing / hvac_modes customize overrides (highest priority).
|
|
|
|
``{"swing": false, "hvac_modes": ["off", "cool", "dry", "fan_only"]}``
|
|
"""
|
|
customize = config_entry.options.get(CONF_CUSTOMIZE, "")
|
|
if not customize:
|
|
return
|
|
try:
|
|
params = json.loads(customize)
|
|
except (ValueError, TypeError):
|
|
return
|
|
if not isinstance(params, dict):
|
|
return
|
|
if "swing" in params:
|
|
self._customize_swing = bool(params["swing"])
|
|
modes = params.get("hvac_modes")
|
|
if isinstance(modes, list):
|
|
valid = {mode.value: mode for mode in self._mode_index}
|
|
wanted: list[HVACMode] = []
|
|
for name in modes:
|
|
hvac = valid.get(str(name))
|
|
if hvac is not None and hvac not in wanted:
|
|
wanted.append(hvac)
|
|
if HVACMode.OFF not in wanted:
|
|
wanted.insert(0, HVACMode.OFF)
|
|
if any(hvac != HVACMode.OFF for hvac in wanted):
|
|
self._customize_hvac_modes = wanted
|
|
presets = params.get("preset_modes")
|
|
if isinstance(presets, list):
|
|
valid_presets = {
|
|
PRESET_NONE,
|
|
PRESET_COMFORT,
|
|
PRESET_ECO,
|
|
PRESET_BOOST,
|
|
PRESET_SLEEP,
|
|
PRESET_AWAY,
|
|
}
|
|
wanted_p: list[str] = []
|
|
for name in presets:
|
|
preset = str(name)
|
|
if preset in valid_presets and preset not in wanted_p:
|
|
wanted_p.append(preset)
|
|
if PRESET_NONE not in wanted_p:
|
|
wanted_p.insert(0, PRESET_NONE)
|
|
# honor even an empty / none-only list (user wants no presets)
|
|
self._customize_preset_modes = wanted_p
|
|
|
|
def _capability_swing(self) -> bool:
|
|
"""Whether swing is available (customize > B5 capability > default).
|
|
|
|
Returns:
|
|
``True`` when the swing control should be exposed.
|
|
|
|
"""
|
|
if self._customize_swing is not None:
|
|
return self._customize_swing
|
|
caps = getattr(self._device, "capabilities", {})
|
|
if "swing_vertical" in caps or "swing_horizontal" in caps:
|
|
return bool(caps.get("swing_vertical") or caps.get("swing_horizontal"))
|
|
return True
|
|
|
|
@property
|
|
def hvac_modes(self) -> list[HVACMode]:
|
|
"""hvac_modes: customize > B5 capabilities > default full set.
|
|
|
|
Read dynamically so capabilities decoded after the first refresh are
|
|
reflected (they are not yet available when the entity is created).
|
|
"""
|
|
if self._customize_hvac_modes is not None:
|
|
return self._customize_hvac_modes
|
|
caps = getattr(self._device, "capabilities", {})
|
|
if not caps:
|
|
return list(self._mode_index)
|
|
modes = [HVACMode.OFF]
|
|
if caps.get("auto_mode"):
|
|
modes.append(HVACMode.AUTO)
|
|
if caps.get("cool_mode"):
|
|
modes.append(HVACMode.COOL)
|
|
if caps.get("dry_mode"):
|
|
modes.append(HVACMode.DRY)
|
|
if caps.get("heat_mode"):
|
|
modes.append(HVACMode.HEAT)
|
|
# fan-only is always available on AC devices
|
|
modes.append(HVACMode.FAN_ONLY)
|
|
return modes
|
|
|
|
@property
|
|
def preset_modes(self) -> list[str]:
|
|
"""preset_modes: customize > B5 capabilities > default full set.
|
|
|
|
B5 only declares eco and turbo (and heat implies away). comfort and
|
|
sleep have no capability flag, so they are kept unless overridden via
|
|
the customize ``preset_modes`` option.
|
|
"""
|
|
all_presets = [
|
|
PRESET_NONE,
|
|
PRESET_COMFORT,
|
|
PRESET_ECO,
|
|
PRESET_BOOST,
|
|
PRESET_SLEEP,
|
|
PRESET_AWAY,
|
|
]
|
|
if self._customize_preset_modes is not None:
|
|
return self._customize_preset_modes
|
|
caps = getattr(self._device, "capabilities", {})
|
|
if not caps:
|
|
return all_presets
|
|
keep = {
|
|
PRESET_NONE: True,
|
|
PRESET_COMFORT: True,
|
|
PRESET_ECO: bool(caps.get("eco")),
|
|
PRESET_BOOST: bool(caps.get("turbo_cool") or caps.get("turbo_heat")),
|
|
PRESET_SLEEP: True,
|
|
PRESET_AWAY: bool(caps.get("heat_mode")),
|
|
}
|
|
return [preset for preset in all_presets if keep[preset]]
|
|
|
|
@property
|
|
def supported_features(self) -> ClimateEntityFeature:
|
|
"""Midea AC Climate supported features."""
|
|
features = super().supported_features
|
|
if not self._capability_swing():
|
|
features &= ~ClimateEntityFeature.SWING_MODE
|
|
# drop the preset control entirely when no real preset is available
|
|
if not any(preset != PRESET_NONE for preset in self.preset_modes):
|
|
features &= ~ClimateEntityFeature.PRESET_MODE
|
|
return features
|
|
|
|
@property
|
|
def hvac_mode(self) -> HVACMode:
|
|
"""Midea AC Climate hvac mode (device mode int -> fixed map)."""
|
|
if self._device.get_attribute("power"):
|
|
mode = cast("int", self._device.get_attribute("mode"))
|
|
return self._mode_index[mode]
|
|
return HVACMode.OFF
|
|
|
|
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
"""Midea AC Climate set hvac mode (via the fixed map index)."""
|
|
if hvac_mode == HVACMode.OFF:
|
|
self.turn_off()
|
|
else:
|
|
self._device.set_attribute(
|
|
attr="mode",
|
|
value=self._mode_index.index(hvac_mode),
|
|
)
|
|
|
|
def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
|
|
"""Midea AC Climate set temperature (raw mode via the fixed map).
|
|
|
|
``hvac_modes`` is a filtered subset here, so the raw protocol mode must
|
|
come from the fixed ``_mode_index`` and not from the displayed list.
|
|
"""
|
|
if ATTR_TEMPERATURE not in kwargs:
|
|
return
|
|
temperature = float(int((float(kwargs[ATTR_TEMPERATURE]) * 2) + 0.5)) / 2
|
|
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
|
if hvac_mode == HVACMode.OFF:
|
|
self.turn_off()
|
|
else:
|
|
try:
|
|
mode = self._mode_index.index(hvac_mode.lower()) if hvac_mode else None
|
|
self._device.set_target_temperature(
|
|
target_temperature=temperature,
|
|
mode=mode,
|
|
zone=None,
|
|
)
|
|
except ValueError:
|
|
_LOGGER.exception("Error setting temperature with: %s", kwargs)
|
|
|
|
@property
|
|
def fan_mode(self) -> str:
|
|
"""Midea AC Climate fan mode."""
|
|
fan_speed = cast("int", self._device.get_attribute(ACAttributes.fan_speed))
|
|
if fan_speed > FanSpeed.AUTO:
|
|
return str(FAN_AUTO)
|
|
if fan_speed > FanSpeed.FULL_SPEED:
|
|
return str(FAN_FULL_SPEED)
|
|
if fan_speed > FanSpeed.HIGH:
|
|
return str(FAN_HIGH)
|
|
if fan_speed > FanSpeed.MEDIUM:
|
|
return str(FAN_MEDIUM)
|
|
if fan_speed > FanSpeed.LOW:
|
|
return str(FAN_LOW)
|
|
return str(FAN_SILENT)
|
|
|
|
@property
|
|
def fan_modes(self) -> list[str] | None:
|
|
"""fan_modes: B5 capabilities > default full set.
|
|
|
|
Read dynamically so capabilities decoded after the first refresh are
|
|
reflected (they are not yet available when the entity is created).
|
|
|
|
The B5 ``b5_wind_speed`` byte is a single capability profile (1-7), not
|
|
a bitmask: value 1 (``fan_custom``) means the fan is stepless/inverter
|
|
and accepts every speed, so all discrete modes are valid. Mapping
|
|
``fan_custom`` to ``full`` alone collapsed the list to ``["full"]`` on
|
|
such units (issue #904); short-circuit to the full set instead.
|
|
"""
|
|
caps = getattr(self._device, "capabilities", {})
|
|
if not caps:
|
|
return list(self._fan_speeds.keys())
|
|
# stepless/inverter fan: expose every discrete speed, not just "full"
|
|
if caps.get("fan_custom"):
|
|
return list(self._fan_speeds.keys())
|
|
cap_by_fan = {
|
|
FAN_SILENT: "fan_silent",
|
|
FAN_LOW: "fan_low",
|
|
FAN_MEDIUM: "fan_medium",
|
|
FAN_HIGH: "fan_high",
|
|
FAN_FULL_SPEED: "fan_custom",
|
|
FAN_AUTO: "fan_auto",
|
|
}
|
|
modes = [
|
|
name for name in self._fan_speeds if caps.get(cap_by_fan.get(name, ""))
|
|
]
|
|
return modes or list(self._fan_speeds.keys())
|
|
|
|
@property
|
|
def target_temperature_step(self) -> float:
|
|
"""Midea AC Climate target temperature step."""
|
|
return float(
|
|
PRECISION_WHOLE if self._device.temperature_step == 1 else PRECISION_HALVES,
|
|
)
|
|
|
|
@property
|
|
def swing_mode(self) -> str:
|
|
"""Midea AC Climate swing mode."""
|
|
swing_mode = (
|
|
1 if self._device.get_attribute(ACAttributes.swing_vertical) else 0
|
|
) + (2 if self._device.get_attribute(ACAttributes.swing_horizontal) else 0)
|
|
return self._attr_swing_modes[swing_mode]
|
|
|
|
@property
|
|
def current_humidity(self) -> float | None:
|
|
"""Current indoor humidity, or None if unavailable."""
|
|
# fix error humidity, disable indoor_humidity in web UI
|
|
# https://github.com/wuwentao/midea_ac_lan/pull/641
|
|
if not self._indoor_humidity_enabled:
|
|
return None
|
|
raw = self._device.get_attribute("indoor_humidity")
|
|
if isinstance(raw, (int, float)) and raw not in {0, 0xFF}:
|
|
return float(raw)
|
|
# indoor_humidity is 0 or 255, return None
|
|
# https://github.com/wuwentao/midea_ac_lan/pull/614
|
|
return None
|
|
|
|
@property
|
|
def outdoor_temperature(self) -> float:
|
|
"""Midea AC Climate outdoor temperature."""
|
|
return cast(
|
|
"float",
|
|
self._device.get_attribute(ACAttributes.outdoor_temperature),
|
|
)
|
|
|
|
@property
|
|
def min_temp(self) -> float:
|
|
"""Midea AC Climate min temperature (from device capability)."""
|
|
value = self._device.get_attribute(ACAttributes.min_temperature)
|
|
if value is None:
|
|
return TEMPERATURE_MIN
|
|
return cast("float", value)
|
|
|
|
@property
|
|
def max_temp(self) -> float:
|
|
"""Midea AC Climate max temperature (from device capability)."""
|
|
value = self._device.get_attribute(ACAttributes.max_temperature)
|
|
if value is None:
|
|
return TEMPERATURE_MAX
|
|
return cast("float", value)
|
|
|
|
def set_fan_mode(self, fan_mode: str) -> None:
|
|
"""Midea AC Climate set fan mode."""
|
|
fan_speed = self._fan_speeds.get(fan_mode)
|
|
if fan_speed:
|
|
self._device.set_attribute(attr=ACAttributes.fan_speed, value=fan_speed)
|
|
|
|
def set_swing_mode(self, swing_mode: str) -> None:
|
|
"""Midea AC Climate set swing mode."""
|
|
swing = self._attr_swing_modes.index(swing_mode)
|
|
swing_vertical = swing & 1 > 0
|
|
swing_horizontal = swing & 2 > 0
|
|
self._device.set_swing(
|
|
swing_vertical=swing_vertical,
|
|
swing_horizontal=swing_horizontal,
|
|
)
|
|
|
|
|
|
class MideaCCClimate(MideaClimate):
|
|
"""Midea CC Climate Entries Base Class."""
|
|
|
|
_device: MideaCCDevice
|
|
|
|
def __init__(self, device: MideaCCDevice, entity_key: str) -> None:
|
|
"""Midea CC Climate entity init."""
|
|
super().__init__(device, entity_key)
|
|
self._attr_hvac_modes = [
|
|
HVACMode.OFF,
|
|
HVACMode.FAN_ONLY,
|
|
HVACMode.DRY,
|
|
HVACMode.HEAT,
|
|
HVACMode.COOL,
|
|
HVACMode.AUTO,
|
|
]
|
|
self._attr_swing_modes = [SWING_OFF, SWING_ON]
|
|
self._attr_preset_modes = [PRESET_NONE, PRESET_SLEEP, PRESET_ECO]
|
|
|
|
@property
|
|
def fan_modes(self) -> list[str] | None:
|
|
"""Midea CC Climate fan modes."""
|
|
return cast("list", self._device.fan_modes)
|
|
|
|
@property
|
|
def fan_mode(self) -> str:
|
|
"""Midea CC Climate fan mode."""
|
|
return cast("str", self._device.get_attribute(CCAttributes.fan_speed))
|
|
|
|
@property
|
|
def target_temperature_step(self) -> float:
|
|
"""Midea CC Climate target temperature step."""
|
|
return cast(
|
|
"float",
|
|
self._device.get_attribute(CCAttributes.temperature_precision),
|
|
)
|
|
|
|
@property
|
|
def swing_mode(self) -> str:
|
|
"""Midea CC Climate swing mode."""
|
|
return str(
|
|
SWING_ON if self._device.get_attribute(CCAttributes.swing) else SWING_OFF,
|
|
)
|
|
|
|
def set_fan_mode(self, fan_mode: str) -> None:
|
|
"""Midea CC Climate set fan mode."""
|
|
self._device.set_attribute(attr=CCAttributes.fan_speed, value=fan_mode)
|
|
|
|
def set_swing_mode(self, swing_mode: str) -> None:
|
|
"""Midea CC Climate set swing mode."""
|
|
self._device.set_attribute(
|
|
attr=CCAttributes.swing,
|
|
value=swing_mode == SWING_ON,
|
|
)
|
|
|
|
|
|
class MideaCFClimate(MideaClimate):
|
|
"""Midea CF Climate Entries."""
|
|
|
|
_device: MideaCFDevice
|
|
|
|
_attr_target_temperature_step: float | None = PRECISION_WHOLE
|
|
|
|
def __init__(self, device: MideaCFDevice, entity_key: str) -> None:
|
|
"""Midea CF Climate entity init."""
|
|
super().__init__(device, entity_key)
|
|
self._attr_hvac_modes = [
|
|
HVACMode.OFF,
|
|
HVACMode.AUTO,
|
|
HVACMode.COOL,
|
|
HVACMode.HEAT,
|
|
]
|
|
|
|
@property
|
|
def supported_features(self) -> ClimateEntityFeature:
|
|
"""Midea CF Climate supported features."""
|
|
features = ClimateEntityFeature.TARGET_TEMPERATURE
|
|
if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 2):
|
|
features |= ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
|
return features
|
|
|
|
@property
|
|
def min_temp(self) -> float:
|
|
"""Midea CF Climate min temperature."""
|
|
return cast("float", self._device.get_attribute(CFAttributes.min_temperature))
|
|
|
|
@property
|
|
def max_temp(self) -> float:
|
|
"""Midea CF Climate max temperature."""
|
|
return cast("float", self._device.get_attribute(CFAttributes.max_temperature))
|
|
|
|
@property
|
|
def target_temperature_low(self) -> float:
|
|
"""Midea CF Climate target temperature."""
|
|
return cast("float", self._device.get_attribute(CFAttributes.min_temperature))
|
|
|
|
@property
|
|
def target_temperature_high(self) -> float:
|
|
"""Midea CF Climate target temperature high."""
|
|
return cast("float", self._device.get_attribute(CFAttributes.max_temperature))
|
|
|
|
@property
|
|
def current_temperature(self) -> float:
|
|
"""Midea CF Climate current temperature."""
|
|
return cast(
|
|
"float",
|
|
self._device.get_attribute(CFAttributes.current_temperature),
|
|
)
|
|
|
|
|
|
class MideaC3Climate(MideaClimate):
|
|
"""Midea C3 Climate Entries."""
|
|
|
|
_device: MideaC3Device
|
|
|
|
_powers: ClassVar[list[C3Attributes]] = [
|
|
C3Attributes.zone1_power,
|
|
C3Attributes.zone2_power,
|
|
]
|
|
|
|
def __init__(self, device: MideaC3Device, entity_key: str, zone: int) -> None:
|
|
"""Midea C3 Climate entity init."""
|
|
super().__init__(device, entity_key)
|
|
self._zone = zone
|
|
self._attr_hvac_modes = [
|
|
HVACMode.OFF,
|
|
HVACMode.AUTO,
|
|
HVACMode.COOL,
|
|
HVACMode.HEAT,
|
|
]
|
|
self._power_attr = MideaC3Climate._powers[zone]
|
|
|
|
def _temperature(self, minimum: bool) -> list[str]:
|
|
"""Midea C3 Climate temperature.
|
|
|
|
Returns
|
|
-------
|
|
List of temperatures
|
|
|
|
"""
|
|
# fmt: off
|
|
value = (C3Attributes.temperature_min
|
|
if minimum
|
|
else C3Attributes.temperature_max)
|
|
# fmt: on
|
|
return cast("list[str]", self._device.get_attribute(value))
|
|
|
|
@property
|
|
def supported_features(self) -> ClimateEntityFeature:
|
|
"""Midea C3 Climate supported features."""
|
|
features = ClimateEntityFeature.TARGET_TEMPERATURE
|
|
if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 2):
|
|
features |= ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
|
return features
|
|
|
|
@property
|
|
def target_temperature_step(self) -> float:
|
|
"""Midea C3 Climate target temperature step."""
|
|
zone_temp_type = cast(
|
|
"list[str]",
|
|
self._device.get_attribute(C3Attributes.zone_temp_type),
|
|
)
|
|
return float(
|
|
PRECISION_WHOLE if zone_temp_type[self._zone] else PRECISION_HALVES,
|
|
)
|
|
|
|
@property
|
|
def min_temp(self) -> float:
|
|
"""Midea C3 Climate min temperature."""
|
|
return cast(
|
|
"float",
|
|
self._temperature(True)[self._zone],
|
|
)
|
|
|
|
@property
|
|
def max_temp(self) -> float:
|
|
"""Midea C3 Climate max temperature."""
|
|
return cast(
|
|
"float",
|
|
self._temperature(False)[self._zone],
|
|
)
|
|
|
|
@property
|
|
def target_temperature_low(self) -> float:
|
|
"""Midea C3 Climate target temperature low."""
|
|
return cast(
|
|
"float",
|
|
self._temperature(True)[self._zone],
|
|
)
|
|
|
|
@property
|
|
def target_temperature_high(self) -> float:
|
|
"""Midea C3 Climate target temperature high."""
|
|
return cast(
|
|
"float",
|
|
self._temperature(False)[self._zone],
|
|
)
|
|
|
|
def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
|
|
"""Midea C3 Climate turn on."""
|
|
self._device.set_attribute(attr=self._power_attr, value=True)
|
|
|
|
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
|
|
"""Midea C3 Climate turn off."""
|
|
self._device.set_attribute(attr=self._power_attr, value=False)
|
|
|
|
@property
|
|
def hvac_mode(self) -> HVACMode:
|
|
"""Midea C3 Climate hvac mode."""
|
|
mode = self._device.get_attribute(C3Attributes.mode)
|
|
if self._device.get_attribute(self._power_attr) and isinstance(mode, int):
|
|
return self.hvac_modes[mode]
|
|
return HVACMode.OFF
|
|
|
|
@property
|
|
def target_temperature(self) -> float:
|
|
"""Midea C3 Climate target temperature."""
|
|
target_temperature = cast(
|
|
"list[str]",
|
|
self._device.get_attribute(C3Attributes.target_temperature),
|
|
)
|
|
return cast(
|
|
"float",
|
|
target_temperature[self._zone],
|
|
)
|
|
|
|
@property
|
|
def current_temperature(self) -> float | None:
|
|
"""Midea C3 Climate current temperature."""
|
|
return None
|
|
|
|
def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
|
|
"""Midea C3 Climate set temperature."""
|
|
if ATTR_TEMPERATURE not in kwargs:
|
|
return
|
|
temperature = float(int((float(kwargs[ATTR_TEMPERATURE]) * 2) + 0.5)) / 2
|
|
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
|
if hvac_mode == HVACMode.OFF:
|
|
self.turn_off()
|
|
else:
|
|
try:
|
|
mode = self.hvac_modes.index(hvac_mode.lower()) if hvac_mode else None
|
|
self._device.set_target_temperature(
|
|
target_temperature=temperature,
|
|
mode=mode,
|
|
zone=self._zone,
|
|
)
|
|
except ValueError:
|
|
_LOGGER.exception("Error setting temperature with: %s", kwargs)
|
|
|
|
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
"""Midea C3 Climate set hvac mode."""
|
|
if hvac_mode == HVACMode.OFF:
|
|
self.turn_off()
|
|
else:
|
|
self._device.set_mode(self._zone, self.hvac_modes.index(hvac_mode))
|
|
|
|
|
|
class MideaFBClimate(MideaClimate):
|
|
"""Midea FB Climate Entries."""
|
|
|
|
_device: MideaFBDevice
|
|
|
|
_attr_max_temp: float = 35
|
|
_attr_min_temp: float = 5
|
|
_attr_target_temperature_high: float | None = 35
|
|
_attr_target_temperature_low: float | None = 5
|
|
_attr_target_temperature_step: float | None = PRECISION_WHOLE
|
|
|
|
def __init__(self, device: MideaFBDevice, entity_key: str) -> None:
|
|
"""Midea FB Climate entity init."""
|
|
super().__init__(device, entity_key)
|
|
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT]
|
|
self._attr_preset_modes: list[str] = self._device.modes
|
|
|
|
@property
|
|
def supported_features(self) -> ClimateEntityFeature:
|
|
"""Midea FB Climate supported features."""
|
|
features = (
|
|
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
|
)
|
|
if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 2):
|
|
features |= ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
|
return features
|
|
|
|
@property
|
|
def preset_mode(self) -> str:
|
|
"""Midea FB Climate preset mode."""
|
|
return cast("str", self._device.get_attribute(attr=FBAttributes.mode))
|
|
|
|
@property
|
|
def hvac_mode(self) -> HVACMode:
|
|
"""Midea FB Climate hvac mode."""
|
|
return (
|
|
HVACMode.HEAT
|
|
if self._device.get_attribute(attr=FBAttributes.power)
|
|
else HVACMode.OFF
|
|
)
|
|
|
|
@property
|
|
def current_temperature(self) -> float:
|
|
"""Midea FB Climate current temperature."""
|
|
return cast(
|
|
"float",
|
|
self._device.get_attribute(FBAttributes.current_temperature),
|
|
)
|
|
|
|
def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
|
|
"""Midea FB Climate set temperature."""
|
|
if ATTR_TEMPERATURE not in kwargs:
|
|
return
|
|
temperature = float(int((float(kwargs[ATTR_TEMPERATURE]) * 2) + 0.5)) / 2
|
|
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
|
if hvac_mode == HVACMode.OFF:
|
|
self.turn_off()
|
|
else:
|
|
self._device.set_target_temperature(
|
|
target_temperature=temperature,
|
|
mode=None,
|
|
)
|
|
|
|
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
"""Midea FB Climate set hvac mode."""
|
|
if hvac_mode == HVACMode.OFF:
|
|
self.turn_off()
|
|
else:
|
|
self.turn_on()
|
|
|
|
def set_preset_mode(self, preset_mode: str) -> None:
|
|
"""Midea FB Climate set preset mode."""
|
|
self._device.set_attribute(attr=FBAttributes.mode, value=preset_mode)
|