mirror of
https://github.com/mjg59/python-broadlink.git
synced 2024-11-22 07:00:12 +01:00
add support for LifaAir air purifier
This commit is contained in:
parent
730853e5fa
commit
afce48207a
14
README.md
14
README.md
@ -13,6 +13,7 @@ A Python module and CLI for controlling Broadlink devices locally. The following
|
|||||||
- **Curtain motors**: Dooya DT360E-45/20
|
- **Curtain motors**: Dooya DT360E-45/20
|
||||||
- **Thermostats**: Hysen HY02B05H
|
- **Thermostats**: Hysen HY02B05H
|
||||||
- **Hubs**: S3
|
- **Hubs**: S3
|
||||||
|
- **Air purifiers**: LIFAair LM05
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -216,6 +217,19 @@ devices[0].set_state(bulb_colormode=1)
|
|||||||
data = device.check_sensors()
|
data = device.check_sensors()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Air purifiers
|
||||||
|
|
||||||
|
### Fetching purifier state
|
||||||
|
```python3
|
||||||
|
data = device.get_state()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controlling purifier fan
|
||||||
|
```python3
|
||||||
|
device.set_fan_mode(FanMode.TURBO)
|
||||||
|
device.set_fan_speed(50)
|
||||||
|
```
|
||||||
|
|
||||||
## Hubs
|
## Hubs
|
||||||
|
|
||||||
### Discovering subdevices
|
### Discovering subdevices
|
||||||
|
@ -14,6 +14,7 @@ from .light import lb1, lb2
|
|||||||
from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro
|
from .remote import rm, rm4, rm4mini, rm4pro, rmmini, rmminib, rmpro
|
||||||
from .sensor import a1, a2
|
from .sensor import a1, a2
|
||||||
from .switch import bg1, ehc31, mp1, mp1s, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b
|
from .switch import bg1, ehc31, mp1, mp1s, sp1, sp2, sp2s, sp3, sp3s, sp4, sp4b
|
||||||
|
from .purifier import lifaair
|
||||||
|
|
||||||
SUPPORTED_TYPES = {
|
SUPPORTED_TYPES = {
|
||||||
sp1: {
|
sp1: {
|
||||||
@ -207,6 +208,9 @@ SUPPORTED_TYPES = {
|
|||||||
ehc31: {
|
ehc31: {
|
||||||
0x6480: ("EHC31", "BG Electrical"),
|
0x6480: ("EHC31", "BG Electrical"),
|
||||||
},
|
},
|
||||||
|
lifaair: {
|
||||||
|
0x4ec2: ("LM05", "LIFAair"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
156
broadlink/purifier.py
Normal file
156
broadlink/purifier.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
"""Support for air purifiers."""
|
||||||
|
|
||||||
|
import enum
|
||||||
|
|
||||||
|
from . import exceptions as e
|
||||||
|
from .device import Device
|
||||||
|
|
||||||
|
@enum.unique
|
||||||
|
class FanMode(enum.IntEnum):
|
||||||
|
"""Represents mode of the fan."""
|
||||||
|
|
||||||
|
OFF = 0
|
||||||
|
AUTO = 1
|
||||||
|
NIGHT = 2
|
||||||
|
TURBO = 3
|
||||||
|
ANTI_ALLERGY = 4
|
||||||
|
MANUAL = 5
|
||||||
|
|
||||||
|
UNKNOWN = -1
|
||||||
|
|
||||||
|
|
||||||
|
class lifaair(Device):
|
||||||
|
"""Controls a broadcom-based LIFAair air purifier."""
|
||||||
|
|
||||||
|
TYPE = "LIFAAIR"
|
||||||
|
|
||||||
|
FAN_STATE_TO_MODE = {
|
||||||
|
0x81: FanMode.OFF,
|
||||||
|
0xA5: FanMode.AUTO,
|
||||||
|
0x95: FanMode.NIGHT,
|
||||||
|
0x8D: FanMode.TURBO,
|
||||||
|
0x85: FanMode.MANUAL,
|
||||||
|
0x01: None, # fan is offline
|
||||||
|
}
|
||||||
|
|
||||||
|
@enum.unique
|
||||||
|
class _Operation(enum.IntEnum):
|
||||||
|
SET_STATE = 1
|
||||||
|
GET_STATE = 2
|
||||||
|
|
||||||
|
@enum.unique
|
||||||
|
class _Action(enum.IntEnum):
|
||||||
|
SET_FAN_SPEED = 1
|
||||||
|
SET_FAN_MODE = 2
|
||||||
|
|
||||||
|
FAN_MODE_TO_ACTION_ARG = {
|
||||||
|
FanMode.OFF: 1,
|
||||||
|
FanMode.AUTO: 2,
|
||||||
|
FanMode.NIGHT: 6,
|
||||||
|
FanMode.TURBO: 7,
|
||||||
|
FanMode.ANTI_ALLERGY: 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_fan_mode(self, fan_mode: FanMode) -> dict:
|
||||||
|
"""Set mode of the fan. Returns updated state."""
|
||||||
|
if fan_mode == FanMode.MANUAL:
|
||||||
|
return self.set_fan_speed(50)
|
||||||
|
|
||||||
|
action_arg = self.FAN_MODE_TO_ACTION_ARG.get(fan_mode)
|
||||||
|
if action_arg is not None:
|
||||||
|
data = self._send(
|
||||||
|
self._Operation.SET_STATE, self._Action.SET_FAN_MODE, action_arg
|
||||||
|
)
|
||||||
|
return self._decode_state(data)
|
||||||
|
|
||||||
|
return self.get_state()
|
||||||
|
|
||||||
|
def set_fan_speed(self, fan_speed: int) -> dict:
|
||||||
|
"""Set fan speed (0-121). Returns updated state. Note that fan mode will be changed to MANUAL by the device."""
|
||||||
|
data = self._send(
|
||||||
|
self._Operation.SET_STATE, self._Action.SET_FAN_SPEED, fan_speed
|
||||||
|
)
|
||||||
|
return self._decode_state(data)
|
||||||
|
|
||||||
|
def get_state(self) -> dict:
|
||||||
|
"""
|
||||||
|
Return the current state of the purifier as python dict.
|
||||||
|
|
||||||
|
Note that the smart remote we're communicating with contains co2, tvoc and PM2.5 sensors,
|
||||||
|
while temperature, humidity and fan-state are fetched remotely from main unit which can be
|
||||||
|
offline (unplugged from mains, out of range) in which case those keys will be None.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
{
|
||||||
|
"temperature": 24.5, # float, deg C, can be None if main-unit offline
|
||||||
|
"humidity": 41, # int, %, can be None if main-unit offline
|
||||||
|
"co2": 425 # int, ppm
|
||||||
|
"tvoc": 150 # int, ug/m3
|
||||||
|
"pm10": 9 # int, ug/m3 (unsure if this is PM10)
|
||||||
|
"pm2_5": 7 # int, ug/m3 (confirmed PM2.5)
|
||||||
|
"pm1": 5 # int, ug/m3 (unsure if this is PM1.0)
|
||||||
|
"fan_mode": FanMode.AUTO # FanMode enum, can be None if main-unit offline
|
||||||
|
"fan_speed": 50 # int, 0-121
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
data = self._send(self._Operation.GET_STATE)
|
||||||
|
return self._decode_state(data)
|
||||||
|
|
||||||
|
def _decode_state(self, data: bytes) -> dict:
|
||||||
|
raw = self._decode_state_raw(data)
|
||||||
|
fan_mode = self._decode_fan_mode(raw["fan_state"], raw["fan_flags"])
|
||||||
|
isOffline = fan_mode is None
|
||||||
|
return {
|
||||||
|
"temperature": None if isOffline else raw["temperature"] / 10.0,
|
||||||
|
"humidity": None if isOffline else raw["humidity"],
|
||||||
|
"co2": raw["co2"],
|
||||||
|
"tvoc": raw["tvoc"] * 10,
|
||||||
|
"pm10": raw["pm10"],
|
||||||
|
"pm2_5": raw["pm2_5"],
|
||||||
|
"pm1": raw["pm1"],
|
||||||
|
"fan_mode": fan_mode,
|
||||||
|
"fan_speed": raw["fan_speed"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _decode_state_raw(self, data: bytes) -> dict:
|
||||||
|
return {
|
||||||
|
"temperature": data[27] + 256 * data[28],
|
||||||
|
"humidity": data[29],
|
||||||
|
"co2": data[31] + 256 * data[32],
|
||||||
|
"tvoc": data[35] + 256 * data[36],
|
||||||
|
"pm10": data[37] + 256 * data[38],
|
||||||
|
"pm2_5": data[39] + 256 * data[40],
|
||||||
|
"pm1": data[41] + 256 * data[42],
|
||||||
|
"fan_state": data[55],
|
||||||
|
"fan_speed": data[56],
|
||||||
|
"fan_flags": data[57],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _decode_fan_mode(self, fan_state: int, fan_flags: int) -> FanMode:
|
||||||
|
if fan_flags & 0x40 == 0:
|
||||||
|
return FanMode.ANTI_ALLERGY
|
||||||
|
return self.FAN_STATE_TO_MODE.get(fan_state, FanMode.UNKNOWN)
|
||||||
|
|
||||||
|
def _send(self, operation: int, action: int = 0, action_arg: int = 0) -> bytes:
|
||||||
|
"""Send a command to the device."""
|
||||||
|
packet = bytearray(26)
|
||||||
|
packet[0x02] = 0xA5
|
||||||
|
packet[0x03] = 0xA5
|
||||||
|
packet[0x04] = 0x5A
|
||||||
|
packet[0x05] = 0x5A
|
||||||
|
packet[0x08] = operation & 0xFF
|
||||||
|
packet[0x0A] = 0x0C
|
||||||
|
packet[0x0E] = action & 0xFF
|
||||||
|
packet[0x0F] = action_arg & 0xFF
|
||||||
|
|
||||||
|
checksum = sum(packet, 0xBEAF) & 0xFFFF
|
||||||
|
packet[0x06] = checksum & 0xFF
|
||||||
|
packet[0x07] = checksum >> 8
|
||||||
|
|
||||||
|
packet_len = len(packet) - 2
|
||||||
|
packet[0x00] = packet_len & 0xFF
|
||||||
|
packet[0x01] = packet_len >> 8
|
||||||
|
|
||||||
|
resp = self.send_packet(0x6A, packet)
|
||||||
|
e.check_error(resp[0x22:0x24])
|
||||||
|
return self.decrypt(resp[0x38:])
|
44
cli/test_lifaair
Normal file
44
cli/test_lifaair
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import time
|
||||||
|
import broadlink
|
||||||
|
from broadlink.purifier import lifaair, FanMode
|
||||||
|
|
||||||
|
def print_state(state: dict):
|
||||||
|
temperature = state["temperature"]
|
||||||
|
humidity = state["humidity"]
|
||||||
|
print("-> Temperature=%s Humidity=%s CO2=%4dppm TVOC=%4dug/m3 PM2.5=%2dug/m3 FanSpeed=%d (%s)" % (
|
||||||
|
"%2.1fC" % temperature if temperature is not None else "-----",
|
||||||
|
"%2d%%" %humidity if humidity is not None else "---",
|
||||||
|
state["co2"],
|
||||||
|
state["tvoc"],
|
||||||
|
state["pm2_5"],
|
||||||
|
state["fan_speed"],
|
||||||
|
state["fan_mode"]))
|
||||||
|
|
||||||
|
print("Searching for lifaair devices... ")
|
||||||
|
dev: lifaair = next(dev for dev in broadlink.discover() if isinstance(dev, lifaair))
|
||||||
|
print("Found %s" % dev)
|
||||||
|
|
||||||
|
print("Authenticating... ", end="")
|
||||||
|
dev.auth()
|
||||||
|
print("OK (id=%d, key=%s)" % (dev.id, dev.aes.algorithm.key.hex()))
|
||||||
|
|
||||||
|
print("Getting firmware version... ", end="")
|
||||||
|
print(dev.get_fwversion())
|
||||||
|
|
||||||
|
print("Getting state...")
|
||||||
|
print_state(dev.get_state())
|
||||||
|
|
||||||
|
for mode in FanMode:
|
||||||
|
print("Setting fan mode to %s" % mode)
|
||||||
|
print_state(dev.set_fan_mode(mode))
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
for speed in (0, 60, 121):
|
||||||
|
print("Setting fan speed to %d" % speed)
|
||||||
|
print_state(dev.set_fan_speed(speed))
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
print("Monitoring state...")
|
||||||
|
while True:
|
||||||
|
print_state(dev.get_state())
|
||||||
|
time.sleep(1)
|
Loading…
Reference in New Issue
Block a user