Skip to content

Commit

Permalink
feat: Add full support for Xiaomi Purifier Elite (zhimi.airp.meb1)
Browse files Browse the repository at this point in the history
- Implemented unique features for Xiaomi Purifier Elite:
- Added specific mappings for zhimi.airp.meb1.
- Updated status output to include PM10 and other key metrics.
- Enhanced logging for device debugging.

This resolves issue #1902 on GitHub.
  • Loading branch information
bairnhard committed Dec 23, 2024
1 parent edb06c5 commit 93cbd57
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 16 deletions.
72 changes: 60 additions & 12 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,23 +1,71 @@
# Byte-compiled files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
*.egg
*.egg-info/
dist/
build/
eggs/
sdist/
wheels/

__pycache__
.idea/
.cache/
.mypy_cache/
.tox/
.venv/
# Installer logs
pip-log.txt
pip-delete-this-directory.txt

.coverage
# Virtual environments
.env/
.venv/
env/
venv/
ENV/
env.bak/
venv.bak/

# generated apidocs
docs/_build/
docs/api/
# Pytest
.cache/
.tox/

# IDE configurations
.idea/
.vscode/
.vscode/settings.json

# pycharm shenanigans
*.orig
*_BACKUP_*
*_BASE_*
*_LOCAL_*
*_REMOTE_*

# Coverage reports
.coverage
*.cover
*.coverage.*

# Testing files
test-results/
.nox/

# MyPy
.mypy_cache/

# Sphinx documentation
docs/_build/
docs/api/

# Local files
*.log
*.swp
.DS_Store
*.tmp
*.temp
*.bak

# Git metadata
*.rej
*.un~
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ integration, this library supports also the following devices:
* Xiaomi Smart Pet Water Dispenser (mmgg.pet_waterer.s1, s4, wi11)
* Xiaomi Mi Smart Humidifer S (jsqs, jsq5)
* Xiaomi Mi Robot Vacuum Mop 2 (Pro+, Ultra)
* Xiaomi Air Purifier Elite (zhimi.airp.meb1)

*Feel free to create a pull request to add support for new devices as
well as additional features for already supported ones.*
Expand Down
149 changes: 145 additions & 4 deletions miio/integrations/zhimi/airpurifier/airpurifier_miot.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@
# Screen
"led_brightness": {"siid": 13, "piid": 2},
# Device Display Unit
"device-display-unit": {"siid": 14, "piid": 1},
"device_display_unit": {"siid": 14, "piid": 1},
}

# https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-za1:2
Expand Down Expand Up @@ -258,11 +258,49 @@
"filter_rfid_tag": {"siid": 14, "piid": 1},
"filter_rfid_product_id": {"siid": 14, "piid": 3},
# Device Display Unit
"device-display-unit": {"siid": 16, "piid": 1},
"device_display_unit": {"siid": 16, "piid": 1},
# Other
"gestures": {"siid": 15, "piid": 13},
}

# https://home.miot-spec.com/spec/zhimi.airp.meb1
_MAPPING_MEB1 = {
# Air Purifier (siid=2)
"power": {"siid": 2, "piid": 1},
"fault": {"siid": 2, "piid": 2},
"mode": {"siid": 2, "piid": 4},
"fan_level": {"siid": 2, "piid": 5},
"plasma": {"siid": 2, "piid": 6},
"uv": {"siid": 2, "piid": 7},
# Environment (siid=3)
"pm2_5_density": {"siid": 3, "piid": 4},
"pm10_density": {"siid": 3, "piid": 8},
"aqi": {"siid": 3, "piid": 9},
"humidity": {"siid": 3, "piid": 1},
"temperature": {"siid": 3, "piid": 7},
# Filter (siid=4)
"filter_life_remaining": {"siid": 4, "piid": 1},
"filter_hours_used": {"siid": 4, "piid": 3},
# Alarm (siid=6)
"buzzer": {"siid": 6, "piid": 1},
"buzzer_volume": {"siid": 6, "piid": 2},
# Physical Control Locked (siid=8)
"child_lock": {"siid": 8, "piid": 1},
# Custom Service (siid=9)
"motor_speed": {"siid": 9, "piid": 1},
"reboot_cause": {"siid": 9, "piid": 8},
"country_code": {"siid": 9, "piid": 11},
# AQI (siid=11)
"aqi_realtime_update_duration": {"siid": 11, "piid": 4},
# RFID (siid=12)
"filter_rfid_tag": {"siid": 12, "piid": 1},
"filter_rfid_product_id": {"siid": 12, "piid": 3},
# Screen (siid=13)
"led_brightness": {"siid": 13, "piid": 2},
# Device Display Unit (siid=15)
"temperature_display_unit": {"siid": 15, "piid": 1},
}


_MAPPINGS = {
"zhimi.airpurifier.ma4": _MAPPING, # airpurifier 3
Expand All @@ -281,6 +319,7 @@
"zhimi.airpurifier.rma2": _MAPPING_RMA2, # airpurifier 4 lite
"zhimi.airp.rmb1": _MAPPING_RMB1, # airpurifier 4 lite
"zhimi.airpurifier.za1": _MAPPING_ZA1, # smartmi air purifier
"zhimi.airp.meb1": _MAPPING_MEB1, # air purifier elite
}

# Models requiring reversed led brightness value
Expand All @@ -290,6 +329,7 @@
"zhimi.airp.mb5a",
"zhimi.airp.vb4",
"zhimi.airp.rmb1",
"zhimi.airp.meb1",
]


Expand All @@ -307,6 +347,14 @@ class LedBrightness(enum.Enum):
Off = 2


class FaultCode(enum.Enum):
NO_FAULT = 0
SENSOR_PM_ERROR = 1
SENSOR_HUM_ERROR = 2
NO_FILTER = 4
UNKNOWN_ERROR = -1


class AirPurifierMiotStatus(DeviceStatus):
"""Container for status reports from the air purifier.
Expand Down Expand Up @@ -429,11 +477,66 @@ def temperature(self) -> Optional[float]:
return round(temperate, 1) if temperate is not None else None

@property
@sensor("PM10 Density", unit="μg/m³")
def pm10_density(self) -> Optional[float]:
"""Current temperature, if available."""
"""Current PM10 density, if available."""
pm10_density = self.data.get("pm10_density")
return round(pm10_density, 1) if pm10_density is not None else None

@property
@sensor("PM2.5 Density", unit="μg/m³")
def pm25_density(self) -> Optional[float]:
"""Return the PM2.5 density."""
return self.data.get("pm2_5_density")

Check warning on line 490 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L490

Added line #L490 was not covered by tests

@property
def is_plasma_on(self) -> bool:
"""Return True if plasma is on."""
return bool(self.data.get("plasma"))

Check warning on line 495 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L495

Added line #L495 was not covered by tests

@property
@setting("Plasma", setter_name="set_plasma")
def plasma(self) -> str:
"""Plasma state."""
return "on" if self.is_plasma_on else "off"

Check warning on line 501 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L501

Added line #L501 was not covered by tests

@property
def is_uv_on(self) -> bool:
"""Return True if UV is on."""
return bool(self.data.get("uv"))

Check warning on line 506 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L506

Added line #L506 was not covered by tests

@property
@setting("UV", setter_name="set_uv")
def uv(self) -> str:
"""UV state."""
return "on" if self.is_uv_on else "off"

Check warning on line 512 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L512

Added line #L512 was not covered by tests

@property
@sensor("Fault Code", unit="")
def fault(self) -> Optional[FaultCode]:
"""Return fault code if any."""
fault_code = self.data.get("fault")
try:
return FaultCode(fault_code) if fault_code is not None else None
except ValueError:
_LOGGER.warning("Unknown fault code: %s", fault_code)
return None

Check warning on line 523 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L518-L523

Added lines #L518 - L523 were not covered by tests

@property
def temperature_display_unit(self) -> Optional[int]:
"""Return temperature display unit."""
return self.data.get("temperature_display_unit")

Check warning on line 528 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L528

Added line #L528 was not covered by tests

@property
def country_code(self) -> Optional[int]:
"""Return country code."""
return self.data.get("country_code")

Check warning on line 533 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L533

Added line #L533 was not covered by tests

@property
def reboot_cause(self) -> Optional[int]:
"""Return reboot cause."""
return self.data.get("reboot_cause")

Check warning on line 538 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L538

Added line #L538 was not covered by tests

@property
def fan_level(self) -> Optional[int]:
"""Current fan level."""
Expand Down Expand Up @@ -530,13 +633,19 @@ class AirPurifierMiot(MiotDevice):
default_output=format_output(
"",
"Power: {result.power}\n"
"Fault Code: {result.fault}\n"
"Fault Description: {result.fault_description}\n"
"Plasma: {result.plasma}\n"
"UV: {result.uv}\n"
"Anion: {result.anion}\n"
"AQI: {result.aqi} μg/m³\n"
"TVOC: {result.tvoc}\n"
"Average AQI: {result.average_aqi} μg/m³\n"
"Humidity: {result.humidity} %\n"
"Temperature: {result.temperature} °C\n"
"Temperature Unit: {result.temperature_display_unit}\n"
"PM10 Density: {result.pm10_density} μg/m³\n"
"PM2.5 Density: {result.pm25_density} μg/m³\n"
"Fan Level: {result.fan_level}\n"
"Mode: {result.mode}\n"
"LED: {result.led}\n"
Expand All @@ -555,7 +664,9 @@ class AirPurifierMiot(MiotDevice):
"Motor speed: {result.motor_speed} rpm\n"
"Filter RFID product id: {result.filter_rfid_product_id}\n"
"Filter RFID tag: {result.filter_rfid_tag}\n"
"Filter type: {result.filter_type}\n",
"Filter type: {result.filter_type}\n"
"Country Code: {result.country_code}\n"
"Reboot Cause: {result.reboot_cause}\n",
)
)
def status(self) -> AirPurifierMiotStatus:
Expand Down Expand Up @@ -764,3 +875,33 @@ def set_led_brightness_level(self, level: int):
raise ValueError("Invalid brightness level: %s" % level)

return self.set_property("led_brightness_level", level)

@command(
click.argument("plasma", type=bool),
default_output=format_output(
lambda plasma: "Turning on plasma" if plasma else "Turning off plasma"
),
)
def set_plasma(self, plasma: bool):
"""Set plasma on/off."""
if "plasma" not in self._get_mapping():
raise UnsupportedFeatureException(

Check warning on line 888 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L888

Added line #L888 was not covered by tests
"Unsupported plasma for model '%s'" % self.model
)

return self.set_property("plasma", plasma)

Check warning on line 892 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L892

Added line #L892 was not covered by tests

@command(
click.argument("uv", type=bool),
default_output=format_output(
lambda uv: "Turning on UV" if uv else "Turning off UV"
),
)
def set_uv(self, uv: bool):
"""Set UV on/off."""
if "uv" not in self._get_mapping():
raise UnsupportedFeatureException(

Check warning on line 903 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L903

Added line #L903 was not covered by tests
"Unsupported UV for model '%s'" % self.model
)

return self.set_property("uv", uv)

Check warning on line 907 in miio/integrations/zhimi/airpurifier/airpurifier_miot.py

View check run for this annotation

Codecov / codecov/patch

miio/integrations/zhimi/airpurifier/airpurifier_miot.py#L907

Added line #L907 was not covered by tests

0 comments on commit 93cbd57

Please sign in to comment.