Skip to content

Commit 77e1baa

Browse files
olarodeswuwentao
andauthored
feat(ad): add ad device support (#362)
Co-authored-by: Hello World <wuwentao2000@126.com> Co-authored-by: Wentao Wu <wuwentao@canaan-creative.com>
1 parent 3a4cef7 commit 77e1baa

File tree

3 files changed

+301
-0
lines changed

3 files changed

+301
-0
lines changed

midealocal/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class DeviceType(IntEnum):
1212
A0 = 0xA0
1313
A1 = 0xA1
1414
AC = 0xAC
15+
AD = 0xAD
1516
B0 = 0xB0
1617
B1 = 0xB1
1718
B3 = 0xB3

midealocal/devices/ad/__init__.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""Midea local AD device."""
2+
3+
import logging
4+
from enum import StrEnum
5+
from typing import Any
6+
7+
from midealocal.const import DeviceType, ProtocolVersion
8+
from midealocal.device import MideaDevice
9+
10+
from .message import Message21Query, Message31Query, MessageADResponse
11+
12+
_LOGGER = logging.getLogger(__name__)
13+
14+
STANDBY_DETECT_LENGTH = 2
15+
16+
17+
class DeviceAttributes(StrEnum):
18+
"""Midea AD device attributes."""
19+
20+
temperature = "temperature"
21+
humidity = "humidity"
22+
tvoc = "tvoc"
23+
co2 = "co2"
24+
pm25 = "pm25"
25+
hcho = "hcho"
26+
presets_function = "presets_function"
27+
fall_asleep_status = "fall_asleep_status"
28+
portable_sense = "portable_sense"
29+
night_mode = "night_mode"
30+
screen_extinction_timeout = "screen_extinction_timeout"
31+
screen_status = "screen_status"
32+
led_status = "led_status"
33+
arofene_link = "arofene_link"
34+
header_exist = "header_exist"
35+
radar_exist = "radar_exist"
36+
header_led_status = "header_led_status"
37+
temperature_raw = "temperature_raw"
38+
humidity_raw = "humidity_raw"
39+
temperature_compensate = "temperature_compensate"
40+
humidity_compensate = "humidity_compensate"
41+
42+
43+
class MideaADDevice(MideaDevice):
44+
"""Midea AD device."""
45+
46+
def __init__(
47+
self,
48+
name: str,
49+
device_id: int,
50+
ip_address: str,
51+
port: int,
52+
token: str,
53+
key: str,
54+
device_protocol: ProtocolVersion,
55+
model: str,
56+
subtype: int,
57+
customize: str, # noqa: ARG002
58+
) -> None:
59+
"""Initialize Midea AD device."""
60+
super().__init__(
61+
name=name,
62+
device_id=device_id,
63+
device_type=DeviceType.AD,
64+
ip_address=ip_address,
65+
port=port,
66+
token=token,
67+
key=key,
68+
device_protocol=device_protocol,
69+
model=model,
70+
subtype=subtype,
71+
attributes={
72+
DeviceAttributes.temperature: None,
73+
DeviceAttributes.humidity: None,
74+
DeviceAttributes.tvoc: None,
75+
DeviceAttributes.co2: None,
76+
DeviceAttributes.pm25: None,
77+
DeviceAttributes.hcho: None,
78+
DeviceAttributes.presets_function: False,
79+
DeviceAttributes.fall_asleep_status: False,
80+
DeviceAttributes.portable_sense: False,
81+
DeviceAttributes.night_mode: False,
82+
DeviceAttributes.screen_extinction_timeout: None,
83+
DeviceAttributes.screen_status: False,
84+
DeviceAttributes.led_status: False,
85+
DeviceAttributes.arofene_link: False,
86+
DeviceAttributes.header_exist: False,
87+
DeviceAttributes.radar_exist: False,
88+
DeviceAttributes.header_led_status: False,
89+
DeviceAttributes.temperature_raw: None,
90+
DeviceAttributes.humidity_raw: None,
91+
DeviceAttributes.temperature_compensate: None,
92+
DeviceAttributes.humidity_compensate: None,
93+
},
94+
)
95+
96+
def build_query(self) -> list[Message21Query | Message31Query]:
97+
"""Midea AD device build query."""
98+
return [
99+
Message21Query(self._message_protocol_version),
100+
Message31Query(self._message_protocol_version),
101+
]
102+
103+
def process_message(self, msg: bytes) -> dict[str, Any]:
104+
"""Midea AD device process message."""
105+
message = MessageADResponse(msg)
106+
_LOGGER.debug("[%s] Received: %s", self.device_id, message)
107+
new_status = {}
108+
for status in self._attributes:
109+
if hasattr(message, str(status)):
110+
value = getattr(message, str(status))
111+
self._attributes[status] = value
112+
new_status[str(status)] = self._attributes[status]
113+
return new_status
114+
115+
def set_attribute(self, attr: str, value: bool | int | str) -> None:
116+
"""Midea AD device set attribute."""
117+
118+
def set_customize(self, customize: str) -> None:
119+
"""Midea AD device set customize."""
120+
121+
122+
class MideaAppliance(MideaADDevice):
123+
"""Midea AD appliance."""

midealocal/devices/ad/message.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""Midea local AD message."""
2+
3+
from midealocal.const import DeviceType
4+
from midealocal.crc8 import calculate
5+
from midealocal.message import (
6+
ListTypes,
7+
MessageBody,
8+
MessageRequest,
9+
MessageResponse,
10+
MessageType,
11+
)
12+
13+
MAX_MSG_SERIAL_NUM = 254
14+
15+
16+
class MessageADBase(MessageRequest):
17+
"""AD message base."""
18+
19+
_message_serial = 0
20+
21+
def __init__(
22+
self,
23+
protocol_version: int,
24+
message_type: MessageType,
25+
body_type: ListTypes,
26+
) -> None:
27+
"""Initialize AD message base."""
28+
super().__init__(
29+
device_type=DeviceType.AD,
30+
protocol_version=protocol_version,
31+
message_type=message_type,
32+
body_type=body_type,
33+
)
34+
MessageADBase._message_serial += 1
35+
if MessageADBase._message_serial >= MAX_MSG_SERIAL_NUM:
36+
MessageADBase._message_serial = 1
37+
self._message_id = MessageADBase._message_serial
38+
39+
@property
40+
def _body(self) -> bytearray:
41+
raise NotImplementedError
42+
43+
@property
44+
def body(self) -> bytearray:
45+
"""AD message base body."""
46+
body = bytearray([self.body_type]) + self._body + bytearray([self._message_id])
47+
body.append(calculate(body))
48+
return body
49+
50+
51+
class Message21Query(MessageADBase):
52+
"""AD X21 message query."""
53+
54+
def __init__(self, protocol_version: int) -> None:
55+
"""Initialize AD X21 message query."""
56+
super().__init__(
57+
protocol_version=protocol_version,
58+
message_type=MessageType.query,
59+
body_type=ListTypes.X21,
60+
)
61+
62+
@property
63+
def _body(self) -> bytearray:
64+
return bytearray(
65+
[
66+
0x01,
67+
],
68+
)
69+
70+
71+
class Message31Query(MessageADBase):
72+
"""AD X31 message query."""
73+
74+
def __init__(self, protocol_version: int) -> None:
75+
"""Initialize AD X31 message query."""
76+
super().__init__(
77+
protocol_version=protocol_version,
78+
message_type=MessageType.query,
79+
body_type=ListTypes.X31,
80+
)
81+
82+
@property
83+
def _body(self) -> bytearray:
84+
return bytearray(
85+
[
86+
0x01,
87+
],
88+
)
89+
90+
91+
class X31MessageBody(MessageBody):
92+
"""AD X31 message general body."""
93+
94+
def __init__(self, body: bytearray) -> None:
95+
"""Initialize AD X31 message general body."""
96+
super().__init__(body)
97+
self.screen_status = body[2] > 0 if body[2] != ListTypes.FF else None
98+
self.led_status = body[3] > 0 if body[3] != ListTypes.FF else None
99+
self.arofene_link = body[4] > 0 if body[4] != ListTypes.FF else None
100+
self.header_exist = body[5] > 0 if body[5] != ListTypes.FF else None
101+
self.radar_exist = body[6] > 0 if body[6] != ListTypes.FF else None
102+
self.header_led_status = body[7] > 0 if body[7] != ListTypes.FF else None
103+
self.temperature_raw = (
104+
(body[8] << 8) + body[9] if body[8] != ListTypes.FF else None
105+
)
106+
self.humidity_raw = (
107+
(body[10] << 8) + body[11] if body[10] != ListTypes.FF else None
108+
)
109+
self.temperature_compensate = (
110+
(body[12] << 8) + body[13] if body[1] > ListTypes.X0D else None
111+
)
112+
self.humidity_compensate = (
113+
(body[14] << 8) + body[15] if body[1] > ListTypes.X0D else None
114+
)
115+
116+
117+
class X21MessageBody(MessageBody):
118+
"""AD X21 message general body."""
119+
120+
def __init__(self, body: bytearray) -> None:
121+
"""Initialize AD X21 message general body."""
122+
super().__init__(body)
123+
self.portable_sense = body[2] > 0
124+
self.night_mode = body[3] > 0
125+
self.screen_extinction_timeout = body[4] if (body[4] != ListTypes.FF) else None
126+
127+
128+
class ADNotifyMessageBody(MessageBody):
129+
"""AD message notify body."""
130+
131+
def __init__(self, body: bytearray) -> None:
132+
"""Initialize AD message notify body."""
133+
super().__init__(body)
134+
if body[1] == 0x01:
135+
self.temperature = (
136+
((((body[3] << 8) + body[4]) - 65535) - 1) / 100
137+
if body[3] >= ListTypes.X80 # >= 128
138+
else ((body[3] << 8) + body[4]) / 100
139+
)
140+
self.humidity = (
141+
((body[5] << 8) + body[6]) / 100 if body[5] != ListTypes.FF else None
142+
)
143+
self.tvoc = ((body[7] << 8) + body[8]) if body[7] != ListTypes.FF else None
144+
self.pm25 = ((body[9] << 8) + body[10]) if body[9] != ListTypes.FF else None
145+
self.co2 = (
146+
((body[11] << 8) + body[12]) if body[11] != ListTypes.FF else None
147+
)
148+
self.hcho = (
149+
((body[13] << 8) + body[14]) / 0.1 if body[13] != ListTypes.FF else None
150+
)
151+
self.arofene_link = (
152+
((body[16] & 0x01) > 0) if body[16] != ListTypes.FF else None
153+
)
154+
self.radar_exist = (
155+
((body[16] & 0x02) > 0) if body[16] != ListTypes.FF else None
156+
)
157+
elif body[1] == ListTypes.X04:
158+
if body[3] == ListTypes.X01:
159+
self.presets_function = body[4] == 0x01
160+
elif body[3] == ListTypes.X02:
161+
self.fall_asleep_status = body[4] == 0x01
162+
163+
164+
class MessageADResponse(MessageResponse):
165+
"""AD message response."""
166+
167+
def __init__(self, message: bytes) -> None:
168+
"""Initialize AD message response."""
169+
super().__init__(bytearray(message))
170+
171+
if self._body_type == ListTypes.X11:
172+
self.set_body(ADNotifyMessageBody(super().body))
173+
elif self._body_type == ListTypes.X21:
174+
self.set_body(X21MessageBody(super().body))
175+
elif self._body_type == ListTypes.X31:
176+
self.set_body(X31MessageBody(super().body))
177+
self.set_attr()

0 commit comments

Comments
 (0)