Skip to content

Commit 93cbd57

Browse files
committed
feat: Add full support for Xiaomi Purifier Elite (zhimi.airp.meb1)
- 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.
1 parent edb06c5 commit 93cbd57

File tree

3 files changed

+206
-16
lines changed

3 files changed

+206
-16
lines changed

.gitignore

+60-12
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,71 @@
1+
# Byte-compiled files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
*.egg
111
*.egg-info/
12+
dist/
13+
build/
14+
eggs/
15+
sdist/
16+
wheels/
217

3-
__pycache__
4-
.idea/
5-
.cache/
6-
.mypy_cache/
7-
.tox/
8-
.venv/
18+
# Installer logs
19+
pip-log.txt
20+
pip-delete-this-directory.txt
921

10-
.coverage
22+
# Virtual environments
23+
.env/
24+
.venv/
25+
env/
26+
venv/
27+
ENV/
28+
env.bak/
29+
venv.bak/
1130

12-
# generated apidocs
13-
docs/_build/
14-
docs/api/
31+
# Pytest
32+
.cache/
33+
.tox/
1534

35+
# IDE configurations
36+
.idea/
37+
.vscode/
1638
.vscode/settings.json
17-
18-
# pycharm shenanigans
1939
*.orig
2040
*_BACKUP_*
2141
*_BASE_*
2242
*_LOCAL_*
2343
*_REMOTE_*
44+
45+
# Coverage reports
46+
.coverage
47+
*.cover
48+
*.coverage.*
49+
50+
# Testing files
51+
test-results/
52+
.nox/
53+
54+
# MyPy
55+
.mypy_cache/
56+
57+
# Sphinx documentation
58+
docs/_build/
59+
docs/api/
60+
61+
# Local files
62+
*.log
63+
*.swp
64+
.DS_Store
65+
*.tmp
66+
*.temp
67+
*.bak
68+
69+
# Git metadata
70+
*.rej
71+
*.un~

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ integration, this library supports also the following devices:
294294
* Xiaomi Smart Pet Water Dispenser (mmgg.pet_waterer.s1, s4, wi11)
295295
* Xiaomi Mi Smart Humidifer S (jsqs, jsq5)
296296
* Xiaomi Mi Robot Vacuum Mop 2 (Pro+, Ultra)
297+
* Xiaomi Air Purifier Elite (zhimi.airp.meb1)
297298

298299
*Feel free to create a pull request to add support for new devices as
299300
well as additional features for already supported ones.*

miio/integrations/zhimi/airpurifier/airpurifier_miot.py

+145-4
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@
222222
# Screen
223223
"led_brightness": {"siid": 13, "piid": 2},
224224
# Device Display Unit
225-
"device-display-unit": {"siid": 14, "piid": 1},
225+
"device_display_unit": {"siid": 14, "piid": 1},
226226
}
227227

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

266+
# https://home.miot-spec.com/spec/zhimi.airp.meb1
267+
_MAPPING_MEB1 = {
268+
# Air Purifier (siid=2)
269+
"power": {"siid": 2, "piid": 1},
270+
"fault": {"siid": 2, "piid": 2},
271+
"mode": {"siid": 2, "piid": 4},
272+
"fan_level": {"siid": 2, "piid": 5},
273+
"plasma": {"siid": 2, "piid": 6},
274+
"uv": {"siid": 2, "piid": 7},
275+
# Environment (siid=3)
276+
"pm2_5_density": {"siid": 3, "piid": 4},
277+
"pm10_density": {"siid": 3, "piid": 8},
278+
"aqi": {"siid": 3, "piid": 9},
279+
"humidity": {"siid": 3, "piid": 1},
280+
"temperature": {"siid": 3, "piid": 7},
281+
# Filter (siid=4)
282+
"filter_life_remaining": {"siid": 4, "piid": 1},
283+
"filter_hours_used": {"siid": 4, "piid": 3},
284+
# Alarm (siid=6)
285+
"buzzer": {"siid": 6, "piid": 1},
286+
"buzzer_volume": {"siid": 6, "piid": 2},
287+
# Physical Control Locked (siid=8)
288+
"child_lock": {"siid": 8, "piid": 1},
289+
# Custom Service (siid=9)
290+
"motor_speed": {"siid": 9, "piid": 1},
291+
"reboot_cause": {"siid": 9, "piid": 8},
292+
"country_code": {"siid": 9, "piid": 11},
293+
# AQI (siid=11)
294+
"aqi_realtime_update_duration": {"siid": 11, "piid": 4},
295+
# RFID (siid=12)
296+
"filter_rfid_tag": {"siid": 12, "piid": 1},
297+
"filter_rfid_product_id": {"siid": 12, "piid": 3},
298+
# Screen (siid=13)
299+
"led_brightness": {"siid": 13, "piid": 2},
300+
# Device Display Unit (siid=15)
301+
"temperature_display_unit": {"siid": 15, "piid": 1},
302+
}
303+
266304

267305
_MAPPINGS = {
268306
"zhimi.airpurifier.ma4": _MAPPING, # airpurifier 3
@@ -281,6 +319,7 @@
281319
"zhimi.airpurifier.rma2": _MAPPING_RMA2, # airpurifier 4 lite
282320
"zhimi.airp.rmb1": _MAPPING_RMB1, # airpurifier 4 lite
283321
"zhimi.airpurifier.za1": _MAPPING_ZA1, # smartmi air purifier
322+
"zhimi.airp.meb1": _MAPPING_MEB1, # air purifier elite
284323
}
285324

286325
# Models requiring reversed led brightness value
@@ -290,6 +329,7 @@
290329
"zhimi.airp.mb5a",
291330
"zhimi.airp.vb4",
292331
"zhimi.airp.rmb1",
332+
"zhimi.airp.meb1",
293333
]
294334

295335

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

309349

350+
class FaultCode(enum.Enum):
351+
NO_FAULT = 0
352+
SENSOR_PM_ERROR = 1
353+
SENSOR_HUM_ERROR = 2
354+
NO_FILTER = 4
355+
UNKNOWN_ERROR = -1
356+
357+
310358
class AirPurifierMiotStatus(DeviceStatus):
311359
"""Container for status reports from the air purifier.
312360
@@ -429,11 +477,66 @@ def temperature(self) -> Optional[float]:
429477
return round(temperate, 1) if temperate is not None else None
430478

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

486+
@property
487+
@sensor("PM2.5 Density", unit="μg/m³")
488+
def pm25_density(self) -> Optional[float]:
489+
"""Return the PM2.5 density."""
490+
return self.data.get("pm2_5_density")
491+
492+
@property
493+
def is_plasma_on(self) -> bool:
494+
"""Return True if plasma is on."""
495+
return bool(self.data.get("plasma"))
496+
497+
@property
498+
@setting("Plasma", setter_name="set_plasma")
499+
def plasma(self) -> str:
500+
"""Plasma state."""
501+
return "on" if self.is_plasma_on else "off"
502+
503+
@property
504+
def is_uv_on(self) -> bool:
505+
"""Return True if UV is on."""
506+
return bool(self.data.get("uv"))
507+
508+
@property
509+
@setting("UV", setter_name="set_uv")
510+
def uv(self) -> str:
511+
"""UV state."""
512+
return "on" if self.is_uv_on else "off"
513+
514+
@property
515+
@sensor("Fault Code", unit="")
516+
def fault(self) -> Optional[FaultCode]:
517+
"""Return fault code if any."""
518+
fault_code = self.data.get("fault")
519+
try:
520+
return FaultCode(fault_code) if fault_code is not None else None
521+
except ValueError:
522+
_LOGGER.warning("Unknown fault code: %s", fault_code)
523+
return None
524+
525+
@property
526+
def temperature_display_unit(self) -> Optional[int]:
527+
"""Return temperature display unit."""
528+
return self.data.get("temperature_display_unit")
529+
530+
@property
531+
def country_code(self) -> Optional[int]:
532+
"""Return country code."""
533+
return self.data.get("country_code")
534+
535+
@property
536+
def reboot_cause(self) -> Optional[int]:
537+
"""Return reboot cause."""
538+
return self.data.get("reboot_cause")
539+
437540
@property
438541
def fan_level(self) -> Optional[int]:
439542
"""Current fan level."""
@@ -530,13 +633,19 @@ class AirPurifierMiot(MiotDevice):
530633
default_output=format_output(
531634
"",
532635
"Power: {result.power}\n"
636+
"Fault Code: {result.fault}\n"
637+
"Fault Description: {result.fault_description}\n"
638+
"Plasma: {result.plasma}\n"
639+
"UV: {result.uv}\n"
533640
"Anion: {result.anion}\n"
534641
"AQI: {result.aqi} μg/m³\n"
535642
"TVOC: {result.tvoc}\n"
536643
"Average AQI: {result.average_aqi} μg/m³\n"
537644
"Humidity: {result.humidity} %\n"
538645
"Temperature: {result.temperature} °C\n"
646+
"Temperature Unit: {result.temperature_display_unit}\n"
539647
"PM10 Density: {result.pm10_density} μg/m³\n"
648+
"PM2.5 Density: {result.pm25_density} μg/m³\n"
540649
"Fan Level: {result.fan_level}\n"
541650
"Mode: {result.mode}\n"
542651
"LED: {result.led}\n"
@@ -555,7 +664,9 @@ class AirPurifierMiot(MiotDevice):
555664
"Motor speed: {result.motor_speed} rpm\n"
556665
"Filter RFID product id: {result.filter_rfid_product_id}\n"
557666
"Filter RFID tag: {result.filter_rfid_tag}\n"
558-
"Filter type: {result.filter_type}\n",
667+
"Filter type: {result.filter_type}\n"
668+
"Country Code: {result.country_code}\n"
669+
"Reboot Cause: {result.reboot_cause}\n",
559670
)
560671
)
561672
def status(self) -> AirPurifierMiotStatus:
@@ -764,3 +875,33 @@ def set_led_brightness_level(self, level: int):
764875
raise ValueError("Invalid brightness level: %s" % level)
765876

766877
return self.set_property("led_brightness_level", level)
878+
879+
@command(
880+
click.argument("plasma", type=bool),
881+
default_output=format_output(
882+
lambda plasma: "Turning on plasma" if plasma else "Turning off plasma"
883+
),
884+
)
885+
def set_plasma(self, plasma: bool):
886+
"""Set plasma on/off."""
887+
if "plasma" not in self._get_mapping():
888+
raise UnsupportedFeatureException(
889+
"Unsupported plasma for model '%s'" % self.model
890+
)
891+
892+
return self.set_property("plasma", plasma)
893+
894+
@command(
895+
click.argument("uv", type=bool),
896+
default_output=format_output(
897+
lambda uv: "Turning on UV" if uv else "Turning off UV"
898+
),
899+
)
900+
def set_uv(self, uv: bool):
901+
"""Set UV on/off."""
902+
if "uv" not in self._get_mapping():
903+
raise UnsupportedFeatureException(
904+
"Unsupported UV for model '%s'" % self.model
905+
)
906+
907+
return self.set_property("uv", uv)

0 commit comments

Comments
 (0)